package hook import ( "errors" "sync" ) var StopPropagation = errors.New("Event hook propagation stopped") // Handler defines a hook handler function. type Handler[T any] func(e T) error // Hook defines a concurrent safe structure for handling event hooks // (aka. callbacks propagation). type Hook[T any] struct { mux sync.RWMutex handlers []Handler[T] } // PreAdd registers a new handler to the hook by prepending it to the existing queue. func (h *Hook[T]) PreAdd(fn Handler[T]) { h.mux.Lock() defer h.mux.Unlock() // minimize allocations by shifting the slice h.handlers = append(h.handlers, nil) copy(h.handlers[1:], h.handlers) h.handlers[0] = fn } // Add registers a new handler to the hook by appending it to the existing queue. func (h *Hook[T]) Add(fn Handler[T]) { h.mux.Lock() defer h.mux.Unlock() h.handlers = append(h.handlers, fn) } // Reset removes all registered handlers. func (h *Hook[T]) Reset() { h.mux.Lock() defer h.mux.Unlock() h.handlers = nil } // Trigger executes all registered hook handlers one by one // with the specified `data` as an argument. // // Optionally, this method allows also to register additional one off // handlers that will be temporary appended to the handlers queue. // // The execution stops when: // - hook.StopPropagation is returned in one of the handlers // - any non-nil error is returned in one of the handlers func (h *Hook[T]) Trigger(data T, oneOffHandlers ...Handler[T]) error { h.mux.RLock() handlers := make([]Handler[T], 0, len(h.handlers)+len(oneOffHandlers)) handlers = append(handlers, h.handlers...) handlers = append(handlers, oneOffHandlers...) // unlock is not deferred to avoid deadlocks when Trigger is called recursive by the handlers h.mux.RUnlock() for _, fn := range handlers { err := fn(data) if err == nil { continue } if errors.Is(err, StopPropagation) { return nil } return err } return nil }