Skip to content

add ListenWithID and StopListening#2

Open
jbert wants to merge 9 commits intoatomicgo:mainfrom
jbert:jb/unregister-listener
Open

add ListenWithID and StopListening#2
jbert wants to merge 9 commits intoatomicgo:mainfrom
jbert:jb/unregister-listener

Conversation

@jbert
Copy link
Copy Markdown

@jbert jbert commented Apr 17, 2026

Hi,

Thanks very much for this library. I wanted to use it to push events out of a websocket, but also wanted to avoid leaking a listener (and any associated resources) if the websocket closed, so I added ListenWithID and StopListening to allow unregistering a listener.

I chose to avoid changing Listen to return an id, since that was a backwards-incompatible change.

Thanks again.

regards,

jb

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds listener lifecycle management to the event package by introducing an ID-returning listener registration method and a way to deregister listeners, addressing use cases like cleaning up websocket listeners on disconnect.

Changes:

  • Add ListenWithID to register listeners while returning an ID for later removal.
  • Add StopListening plus a new ErrUnknownListener error for unknown listener IDs.
  • Update examples to demonstrate listener deregistration.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
event.go Switch listener storage to an ID-keyed map; add ListenWithID, StopListening, and ErrUnknownListener.
examples_test.go Add a runnable example demonstrating unregistering a listener and verifying output stops.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread event.go
Comment on lines 16 to 21
type Event[T any] struct {
listeners []func(T)
listeners map[int]func(T)
mu sync.RWMutex
closed bool
next_id int
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next_id uses snake_case. This is not idiomatic Go and is likely to be reported by stylecheck (ST1003). Rename to nextID (and update getID accordingly).

Copilot uses AI. Check for mistakes.
Comment thread event.go Outdated
// 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)
var listeners []func(T)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop builds a slice via repeated append without preallocating capacity even though len(e.listeners) is known. With prealloc enabled, this may be reported and it also causes unnecessary allocations. Consider make([]func(T), 0, len(e.listeners)) before the loop.

Suggested change
var listeners []func(T)
listeners := make([]func(T), 0, len(e.listeners))

Copilot uses AI. Check for mistakes.
Comment thread event.go Outdated
func (e *Event[T]) Listen(f func(T)) error {
_, err := e.ListenWithID(f)
return err

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s an extra blank line before the closing brace. With gofumpt/whitespace linters enabled, this formatting may fail linting. Please remove the blank line.

Suggested change

Copilot uses AI. Check for mistakes.
Comment thread event.go Outdated
Comment on lines +77 to +88
func (e *Event[T]) ListenWithID(f func(T)) (int, error) {
e.mu.Lock()
defer e.mu.Unlock()

if e.closed {
return ErrEventClosed
return -1, ErrEventClosed
}

e.listeners = append(e.listeners, f)
id := e.getID()

e.listeners[id] = f

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ListenWithID will panic if e.listeners is nil (e.g., when Event is used as a zero value via var e event.Event[T] / new(Event[T])). Previously, the zero value worked because appending to a nil slice is safe. Consider lazily initializing the map under the lock when it’s nil to preserve zero-value usability and avoid panics.

Copilot uses AI. Check for mistakes.
Comment thread event.go
Comment on lines +92 to 102
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
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StopListening is an exported method but it lacks a doc comment. stylecheck typically requires exported identifiers to have a comment that starts with the identifier name. Please add a // StopListening ... comment describing behavior (including what happens when the event is closed or the ID is unknown).

Copilot uses AI. Check for mistakes.
Comment thread examples_test.go Outdated
}

func ExampleEvent_StopListening() {

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a leading blank line inside the function body. This differs from the other examples in this file and may be flagged by gofumpt/whitespace linting. Please remove the initial blank line after the opening brace.

Suggested change

Copilot uses AI. Check for mistakes.
Comment thread examples_test.go
Comment on lines +93 to +96
triggerCount := 0
listen_id, _ := exampleEvent.ListenWithID(func(v string) {
triggerCount++
fmt.Printf("%d - %s\n", triggerCount, v)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listen_id uses snake_case, which is not idiomatic Go and may be flagged by stylecheck (ST1003) even in _test.go files. Rename to listenID for consistency with the rest of the examples.

Copilot uses AI. Check for mistakes.
Comment thread event.go Outdated
Comment on lines +11 to +12
// ErrUnknownListener is returned when attempting to unregister an unknown id
var ErrUnknownListener = errors.New("listener id is unknown")
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported error’s doc comment is missing a trailing period, which is likely to be flagged by stylecheck (ST1005) and is inconsistent with ErrEventClosed’s comment format. Please end the sentence with a period (and consider capitalizing “ID” for consistency in comments/messages).

Suggested change
// ErrUnknownListener is returned when attempting to unregister an unknown id
var ErrUnknownListener = errors.New("listener id is unknown")
// ErrUnknownListener is returned when attempting to unregister an unknown ID.
var ErrUnknownListener = errors.New("listener ID is unknown")

Copilot uses AI. Check for mistakes.
@MarvinJWendt
Copy link
Copy Markdown
Member

MarvinJWendt commented Apr 18, 2026

Hi @jbert, I believe we could return the id on the Listen method. event is currently at the unstable version v0.3.0, so breaking changes are accepted.

Don't worry about the Copilot comments, I will take a closer look at the repo in the next days and implement your change :)

Thanks for contributing!

@jbert
Copy link
Copy Markdown
Author

jbert commented Apr 18, 2026

Hi, I've updated the branch to address the copilot comments and your point about returning the ID from Listen. Thanks again :-)

jb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants