diff --git a/event.go b/event.go index 72e2382..9c4592a 100644 --- a/event.go +++ b/event.go @@ -8,12 +8,16 @@ import ( // ErrEventClosed is returned when an operation is attempted on a closed event. var ErrEventClosed = errors.New("event is closed") +// ErrUnknownListener is returned when attempting to unregister an unknown id. +var ErrUnknownListener = errors.New("listener id is unknown") + // Event represents a generic, thread-safe event system that can handle multiple listeners. // The type parameter T specifies the type of data that the event carries when triggered. type Event[T any] struct { - listeners []func(T) + listeners map[int]func(T) mu sync.RWMutex closed bool + nextID int } // New creates and returns a new Event instance for the specified type T. @@ -33,8 +37,10 @@ func (e *Event[T]) Trigger(value T) error { // Copy the listeners to avoid holding the lock during execution. // This ensures that triggering the event is thread-safe even if listeners are added or removed concurrently. - listeners := make([]func(T), len(e.listeners)) - copy(listeners, e.listeners) + listeners := make([]func(T), 0, len(e.listeners)) + for _, l := range e.listeners { + listeners = append(listeners, l) + } e.mu.RUnlock() var wg sync.WaitGroup @@ -54,20 +60,50 @@ func (e *Event[T]) Trigger(value T) error { // Listen registers a new listener callback function for the event. // The listener will be invoked with the event's data whenever Trigger is called. +// Returns an ID which can be used with StopListening to deregister the listener. // Returns ErrEventClosed if the event has been closed. -func (e *Event[T]) Listen(f func(T)) error { +func (e *Event[T]) Listen(f func(T)) (int, error) { e.mu.Lock() defer e.mu.Unlock() + // Lazy init to allow use of zero value + if e.listeners == nil { + e.listeners = make(map[int]func(T)) + } + if e.closed { - return ErrEventClosed + return -1, ErrEventClosed } - e.listeners = append(e.listeners, f) + id := e.getID() + + e.listeners[id] = f + + return id, nil +} + +// StopListening unregisters a listener, using the ID returned from Listen. +// The callback which was registered with that ID will no longer be called +// and any associated resources will be released. +// If the ID passed in is not registered, ErrUnknownListener will be returned. +func (e *Event[T]) StopListening(id int) error { + e.mu.Lock() + defer e.mu.Unlock() + _, ok := e.listeners[id] + if !ok { + return ErrUnknownListener + } + delete(e.listeners, id) return nil } +func (e *Event[T]) getID() int { + id := e.nextID + e.nextID++ + return id +} + // Close closes the event system, preventing any new listeners from being added or events from being triggered. // After calling Close, any subsequent calls to Trigger or Listen will return ErrEventClosed. // Existing listeners are removed, and resources are cleaned up. diff --git a/event_test.go b/event_test.go index 0e4b82e..ddb03e8 100644 --- a/event_test.go +++ b/event_test.go @@ -1 +1,10 @@ package event + +import "testing" + +func TestZeroValue(t *testing.T) { + var ev Event[string] + + // Can listen to zero value without panicing + ev.Listen(func(v string) { println(v) }) +} diff --git a/examples_test.go b/examples_test.go index 5c63143..6f93151 100644 --- a/examples_test.go +++ b/examples_test.go @@ -83,3 +83,43 @@ func ExampleEvent_Close() { // 2 // 3 } + +func ExampleEvent_StopListening() { + // Create a new event + exampleEvent := event.New[string]() + + // Listen to the event + triggerCount := 0 + listenID, _ := exampleEvent.Listen(func(v string) { + triggerCount++ + fmt.Printf("%d - %s\n", triggerCount, v) + }) + + // Trigger the event + exampleEvent.Trigger("foo") + delay() // delay for deterministic output + exampleEvent.Trigger("bar") + delay() // delay for deterministic output + exampleEvent.Trigger("baz") + + // Time for listeners to process the event + delay() + + // Stop listening + exampleEvent.StopListening(listenID) + + // Trigger the event again + exampleEvent.Trigger("foo") + delay() // delay for deterministic output + exampleEvent.Trigger("bar") + delay() // delay for deterministic output + exampleEvent.Trigger("baz") + + // Keep the program alive + time.Sleep(time.Second) + + // Output: + // 1 - foo + // 2 - bar + // 3 - baz +}