-
Notifications
You must be signed in to change notification settings - Fork 106
refactor: events refactor (Listen, Dispatch) #1133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1133 +/- ##
==========================================
+ Coverage 66.81% 68.54% +1.73%
==========================================
Files 214 267 +53
Lines 14050 15688 +1638
==========================================
+ Hits 9387 10753 +1366
- Misses 4287 4462 +175
- Partials 376 473 +97 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Add comprehensive tests for application_dispatch.go functions: * callListener with various listener types * callListenerHandle with different argument patterns * canUse and convert utility functions * getWildcardListeners and prepareListeners - Add comprehensive tests for application_listen.go functions: * Listen with closures, event slices, and invalid types * Event interface listening and wildcard registration * Error handling for invalid parameters - Add tests for dispatch functionality: * Wildcard listener dispatching * Struct event dispatching with custom signatures * Payload handling in event dispatching - Create custom test event types to avoid conflicts - Use isolated mock instances to prevent test interference - Focus on core functionality coverage while avoiding complex queue mocking This addresses the GitHub Actions codecov feedback and improves overall code quality with better test coverage.
There was a problem hiding this 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 introduces a modern event handling system with new Listen() and Dispatch() methods, providing a more flexible and developer-friendly alternative to the existing Register()/Job() pattern. The refactor adds support for wildcard patterns, dynamic listener registration, multiple event formats, and seamless queue integration while maintaining backward compatibility.
Key Changes
- Added new
Listen()method supporting string events, event interfaces, wildcards, and closures with flexible listener registration - Added new
Dispatch()method for immediate event firing with listener response collection - Implemented thread-safe concurrent event dispatching using
sync.RWMutexandsync.Mapfor wildcard caching
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| contracts/event/events.go | Restructured event interfaces, adding EventListener, EventQueueListener, QueueListener, Signature, and ShouldQueue interfaces; added Listen and Dispatch methods to Instance interface |
| event/application.go | Added new fields (listeners, mu, wildcards, wildcardsCache) to support the new event system; reorganized existing methods |
| event/application_dispatch.go | Implemented Dispatch method with wildcard matching, listener invocation, queue integration, and panic recovery |
| event/application_listen.go | Implemented Listen method supporting multiple event formats, automatic queue registration, and wildcard patterns |
| event/utils.go | Added dynamicQueueJob struct and getEventName helper method for event name resolution |
| event/utils_test.go | Added comprehensive tests for utility functions including dynamicQueueJob methods and event name resolution |
| event/application_test.go | Added extensive tests for Listen, Dispatch, concurrent dispatching, error recovery, and helper methods |
| event/task.go | Added documentation comment for eventArgsToQueueArgs function |
| mocks/event/*.go | Generated mock implementations for new interfaces (Signature, ShouldQueue, QueueListener, EventQueueListener, EventListener) and updated Instance mock |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (l *TestEventQueueListener) Signature() string { | ||
| return "TestEventQueueListener" | ||
| } | ||
|
|
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TestEventQueueListener struct is missing the required Queue method to fully implement the event.EventQueueListener interface. According to the interface definition, EventQueueListener embeds EventListener, Signature, and ShouldQueue, where the interface requires a Queue method at line 20 of contracts/event/events.go. This test struct only implements Handle, ShouldQueue, and Signature, but is missing the Queue method.
Add the missing method:
func (l *TestEventQueueListener) Queue(args ...any) event.Queue {
return event.Queue{
Enable: l.ShouldQueueFunc,
Connection: "",
Queue: "",
}
}| func (l *TestEventQueueListener) Queue(args ...any) event.Queue { | |
| return event.Queue{ | |
| Enable: l.ShouldQueueFunc, | |
| Connection: "", | |
| Queue: "", | |
| } | |
| } |
|
|
||
| for event, wildcard := range app.wildcards { | ||
| if str.Of(eventNameStr).Is(app.getEventName(event)) { | ||
| wildcardListeners = append(wildcardListeners, wildcard...) |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: The method iterates over app.wildcards map while holding only a read lock (RLock in Dispatch), but wildcard listeners can be modified via Listen() which takes a write lock. Although Go's map iteration is safe for concurrent reads, the underlying slice wildcard (line 201) could be modified by another goroutine calling setupWildcardListen (line 125 in application_listen.go), potentially causing a race condition when appending to wildcardListeners.
While this is mitigated by the cache mechanism, consider documenting this behavior or ensuring thread safety during the iteration.
| wildcardListeners = append(wildcardListeners, wildcard...) | |
| // Make a copy of the wildcard slice to avoid race conditions | |
| wildcardCopy := make([]any, len(wildcard)) | |
| copy(wildcardCopy, wildcard) | |
| wildcardListeners = append(wildcardListeners, wildcardCopy...) |
| func (app *Application) setupEvents(e any, listener any) { | ||
| eventName := app.getEventName(e) | ||
|
|
||
| // Register queued listeners with the queue system | ||
| if l, ok := listener.(event.EventQueueListener); ok { | ||
| // Capture event in local scope to avoid closure variable capture bug | ||
| eventCopy := e | ||
| app.queue.Register([]queue.Job{ | ||
| &dynamicQueueJob{ | ||
| signature: l.Signature(), | ||
| handler: func(queueArgs ...any) error { | ||
| // Convert queue args back to event args format | ||
| eventArgs := make([]any, len(queueArgs)) | ||
| copy(eventArgs, queueArgs) | ||
| return l.Handle(eventCopy, eventArgs...) | ||
| }, | ||
| shouldQueue: l.ShouldQueue(), | ||
| queueConfig: l, | ||
| }, | ||
| }) | ||
| } else if l, ok := listener.(event.QueueListener); ok { | ||
| app.queue.Register([]queue.Job{l}) | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setupEvents method registers the same listener to the queue multiple times if called repeatedly with the same listener. When Listen is called multiple times with the same EventQueueListener or QueueListener for different events, the queue.Register call (lines 99-111 and 113) will register duplicate jobs. This could lead to the listener being invoked multiple times from the queue even though it should only be registered once.
Consider tracking already-registered queue jobs or deduplicate based on Signature() before registering with the queue system.
| // Clear cache to ensure fresh wildcard matching | ||
| app.wildcardsCache.Range(func(key, value any) bool { | ||
| app.wildcardsCache.Delete(key) | ||
| return true | ||
| }) | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clearing the entire wildcard cache on every wildcard listener registration is inefficient. If many wildcard listeners are registered (e.g., during application bootstrap), this will repeatedly clear and rebuild the cache. Consider a more selective cache invalidation strategy, such as only invalidating cache entries that match the new wildcard pattern, or deferring cache clearing until the next dispatch operation.
| // Clear cache to ensure fresh wildcard matching | |
| app.wildcardsCache.Range(func(key, value any) bool { | |
| app.wildcardsCache.Delete(key) | |
| return true | |
| }) | |
| } | |
| // Selectively clear cache entries that match the new wildcard pattern | |
| app.wildcardsCache.Range(func(key, value any) bool { | |
| if wildcardMatch(eventName, key.(string)) { | |
| app.wildcardsCache.Delete(key) | |
| } | |
| return true | |
| }) | |
| } | |
| // wildcardMatch checks if the eventName matches the wildcard pattern. | |
| // For example, pattern "user.*" matches eventName "user.created". | |
| func wildcardMatch(pattern, eventName string) bool { | |
| if !strings.Contains(pattern, "*") { | |
| return pattern == eventName | |
| } | |
| parts := strings.Split(pattern, "*") | |
| prefix := parts[0] | |
| return strings.HasPrefix(eventName, prefix) | |
| } |
| func (app *Application) Job(e event.Event, args []event.Arg) event.Task { | ||
| listeners, ok := app.events[e] | ||
| if !ok { | ||
| listeners = make([]event.Listener, 0) | ||
| } | ||
|
|
||
| return NewTask(app.queue, args, e, listeners) | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Job method accesses app.events map without acquiring any lock, but this map could potentially be modified by the Register method concurrently. While the current implementation of Register doesn't seem to support concurrent modification (it only assigns to the map once), accessing a map concurrently with writes can cause a runtime panic. Consider protecting this read operation with app.mu.RLock() and app.mu.RUnlock() for thread safety, consistent with how Dispatch protects its reads.
|
|
||
| // GetEvents returns all registered events and their listeners. | ||
| func (app *Application) GetEvents() map[event.Event][]event.Listener { | ||
| return app.events |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GetEvents method returns a direct reference to the internal app.events map without any locking or defensive copying. This allows external callers to modify the map concurrently, which could cause race conditions with the Register and Job methods. Consider either:
- Protecting the access with
app.mu.RLock()and returning a copy of the map, or - Documenting that the returned map should not be modified by callers.
| return app.events | |
| app.mu.RLock() | |
| defer app.mu.RUnlock() | |
| eventsCopy := make(map[event.Event][]event.Listener, len(app.events)) | |
| for k, v := range app.events { | |
| eventsCopy[k] = v | |
| } | |
| return eventsCopy |
| // call invokes a function with the given arguments safely, recovering from panics. | ||
| // If a panic occurs, it returns nil. This allows event processing to continue | ||
| // even if a single listener panics. | ||
| // | ||
| // Parameters: | ||
| // - fn: The function to invoke | ||
| // - args: The arguments to pass to the function | ||
| // | ||
| // Returns: The first return value from the function, or nil if function panics or returns nothing | ||
| func (app *Application) call(fn reflect.Value, args []reflect.Value) any { | ||
| defer func() { | ||
| // Recover from panics to prevent one bad listener from breaking | ||
| // the entire event dispatch chain | ||
| _ = recover() | ||
| }() |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states "If a panic occurs, it returns nil", but the function silently swallows the panic without any logging or notification. This can make debugging difficult in production environments. Consider at least logging the panic before recovering, or document this behavior more explicitly as a design decision.
Example improvement:
defer func() {
if r := recover(); r != nil {
// Log or handle panic appropriately
// For now, silently recover as per design
}
}()| func TestApplication_Listen(t *testing.T) { | ||
| mockQueue := mocksqueue.NewQueue(t) | ||
| app := NewApplication(mockQueue) | ||
|
|
||
| // Test listening to a string event | ||
| t.Run("ListenStringEvent", func(t *testing.T) { | ||
| eventName := "test.event" | ||
| handler := func() error { return nil } | ||
|
|
||
| err := app.Listen(eventName, handler) | ||
| require.NoError(t, err) | ||
|
|
||
| listeners, exists := app.listeners[eventName] | ||
| require.True(t, exists) | ||
| require.Len(t, listeners, 1) | ||
| }) | ||
|
|
||
| // Test listening to multiple string events | ||
| t.Run("ListenMultipleEvents", func(t *testing.T) { | ||
| events := []string{"event1", "event2"} | ||
| handler := func() error { return nil } | ||
|
|
||
| err := app.Listen(events, handler) | ||
| require.NoError(t, err) | ||
|
|
||
| for _, e := range events { | ||
| listeners, exists := app.listeners[e] | ||
| require.True(t, exists) | ||
| require.Len(t, listeners, 1) | ||
| } | ||
| }) | ||
|
|
||
| // Test listening to a wildcard event | ||
| t.Run("ListenWildcardEvent", func(t *testing.T) { | ||
| eventName := "user.*" | ||
| handler := func() error { return nil } | ||
|
|
||
| err := app.Listen(eventName, handler) | ||
| require.NoError(t, err) | ||
|
|
||
| listeners, exists := app.wildcards[eventName] | ||
| require.True(t, exists) | ||
| require.Len(t, listeners, 1) | ||
|
|
||
| // Verify cache is empty by checking it has no entries | ||
| cacheEmpty := true | ||
| app.wildcardsCache.Range(func(key, value any) bool { | ||
| cacheEmpty = false | ||
| return false // Stop iterating | ||
| }) | ||
| assert.True(t, cacheEmpty, "Cache should be cleared") | ||
| }) | ||
|
|
||
| // Test listening to an event without a listener | ||
| t.Run("ListenNoListener", func(t *testing.T) { | ||
| err := app.Listen("event.name") | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "listener is required") | ||
| }) | ||
|
|
||
| // Test listening with closure - this should fail since closure doesn't match expected pattern | ||
| t.Run("ListenClosure", func(t *testing.T) { | ||
| closure := func(evt *TestEventCustom) error { | ||
| return nil | ||
| } | ||
|
|
||
| err := app.Listen(closure) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "closure must accept exactly one event parameter") | ||
| }) | ||
|
|
||
| // Test listening with invalid closure | ||
| t.Run("ListenInvalidClosure", func(t *testing.T) { | ||
| invalidClosure := func(a, b string) error { | ||
| return nil | ||
| } | ||
|
|
||
| err := app.Listen(invalidClosure) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), "closure must accept exactly one event parameter") | ||
| }) | ||
|
|
||
| // Test listening to event interface | ||
| t.Run("ListenEventInterface", func(t *testing.T) { | ||
| // Create fresh app for this test | ||
| mockQueueLocal := mocksqueue.NewQueue(t) | ||
| appLocal := NewApplication(mockQueueLocal) | ||
|
|
||
| evt := &TestEventCustom{} | ||
| handler := func() error { return nil } | ||
|
|
||
| err := appLocal.Listen(evt, handler) | ||
| require.NoError(t, err) | ||
|
|
||
| listeners, exists := appLocal.listeners["TestEventCustom"] | ||
| require.True(t, exists) | ||
| require.Len(t, listeners, 1) | ||
| }) | ||
|
|
||
| // Test listening to slice of events | ||
| t.Run("ListenEventSlice", func(t *testing.T) { | ||
| // Create fresh app for this test to avoid interference | ||
| mockQueueLocal := mocksqueue.NewQueue(t) | ||
| appLocal := NewApplication(mockQueueLocal) | ||
|
|
||
| events := []event.Event{&TestEventCustom{}, &TestEvent{}} | ||
| handler := func() error { return nil } | ||
|
|
||
| err := appLocal.Listen(events, handler) | ||
| require.NoError(t, err) | ||
|
|
||
| // Check both events were registered | ||
| listeners1, exists1 := appLocal.listeners["TestEventCustom"] | ||
| require.True(t, exists1) | ||
| require.Len(t, listeners1, 1) | ||
|
|
||
| listeners2, exists2 := appLocal.listeners["TestEvent"] | ||
| require.True(t, exists2) | ||
| require.Len(t, listeners2, 1) | ||
| }) | ||
|
|
||
| // Test listening with valid event type that has getEventName | ||
| t.Run("ListenValidEventType", func(t *testing.T) { | ||
| type ValidEvent struct{ Name string } | ||
| validEvent := ValidEvent{Name: "valid"} | ||
| handler := func() error { return nil } | ||
|
|
||
| err := app.Listen(validEvent, handler) | ||
| require.NoError(t, err) | ||
|
|
||
| // Should register under the struct name | ||
| listeners, exists := app.listeners["ValidEvent"] | ||
| require.True(t, exists) | ||
| require.Len(t, listeners, 1) | ||
| }) | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TestApplication_Listen tests don't cover the scenario where multiple listeners are provided in the variadic listener parameter. According to the function signature Listen(events any, listener ...any), multiple listeners could be passed, but the implementation only uses listener[0] (line 52, 55, 59, 62, 65 in application_listen.go). If this is intentional and only one listener is supported, it should be tested and documented. Otherwise, test what happens when multiple listeners are provided.
| func TestApplication_Dispatch(t *testing.T) { | ||
| mockQueue := mocksqueue.NewQueue(t) | ||
| app := NewApplication(mockQueue) | ||
|
|
||
| // Test dispatching a string event | ||
| t.Run("DispatchStringEvent", func(t *testing.T) { | ||
| eventName := "test.event" | ||
| app.listeners = map[string][]any{ | ||
| eventName: { | ||
| func(name string) any { | ||
| assert.Equal(t, eventName, name) | ||
| return "response" | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| responses := app.Dispatch(eventName) | ||
| require.Len(t, responses, 1) | ||
| assert.Equal(t, "response", responses[0]) | ||
| }) | ||
|
|
||
| // Test dispatching an event with payload | ||
| t.Run("DispatchWithPayload", func(t *testing.T) { | ||
| eventName := "user.created" | ||
| payload := []event.Arg{{Value: "testuser", Type: "string"}} | ||
|
|
||
| app.listeners = map[string][]any{ | ||
| eventName: { | ||
| func(name string, username any) any { | ||
| assert.Equal(t, eventName, name) | ||
| assert.Equal(t, "testuser", username) | ||
| return "processed" | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| responses := app.Dispatch(eventName, payload) | ||
| require.Len(t, responses, 1) | ||
| assert.Equal(t, "processed", responses[0]) | ||
| }) | ||
|
|
||
| // Test dispatching to wildcard listeners | ||
| t.Run("DispatchWithWildcardListeners", func(t *testing.T) { | ||
| eventName := "user.registered" | ||
| app.listeners = map[string][]any{} | ||
| app.wildcards = map[string][]any{ | ||
| "user.*": { | ||
| func(name string) any { | ||
| assert.Equal(t, eventName, name) | ||
| return "wildcard" | ||
| }, | ||
| }, | ||
| } | ||
| app.wildcardsCache = sync.Map{} | ||
|
|
||
| responses := app.Dispatch(eventName) | ||
| require.Len(t, responses, 1) | ||
| assert.Equal(t, "wildcard", responses[0]) | ||
| }) | ||
|
|
||
| // Test dispatching a struct event | ||
| t.Run("DispatchStructEvent", func(t *testing.T) { | ||
| evt := &TestEventCustom{} | ||
| app.listeners = map[string][]any{ | ||
| "TestEventCustom": { | ||
| func(e *TestEventCustom) any { | ||
| assert.Equal(t, evt, e) | ||
| return "received" | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| responses := app.Dispatch(evt) | ||
| require.Len(t, responses, 1) | ||
| assert.Equal(t, "received", responses[0]) | ||
| }) | ||
| } |
Copilot
AI
Nov 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TestApplication_Dispatch tests don't verify behavior when queued listeners are dispatched. The test should include cases for:
- Dispatching events with
EventQueueListenerwhereShouldQueue()returns true - Dispatching events with
QueueListenerwhereShouldQueue()returns true - Verifying that the queue's
Jobmethod is called appropriately
This would ensure the integration between event dispatch and queue system works correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return &Application{ | ||
| queue: queue, | ||
| listeners: make(map[string][]any), | ||
| mu: sync.RWMutex{}, |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initializing sync.RWMutex{} explicitly is redundant. The zero value of sync.RWMutex is already a valid, unlocked mutex. You can simply omit this field from the initialization.
| mu: sync.RWMutex{}, |
|
|
||
| switch e := events.(type) { | ||
| case string: | ||
| app.setupEvents(e, listener[0]) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When Listen() is called with multiple listeners (variadic listener ...any), only listener[0] is used. The remaining listeners are silently ignored. This could lead to bugs where developers pass multiple listeners expecting all of them to be registered. Consider either processing all listeners in the slice or documenting that only the first listener is used, or better yet, return an error if more than one listener is provided.
| func (app *Application) call(fn reflect.Value, args []reflect.Value) any { | ||
| defer func() { | ||
| // Recover from panics to prevent one bad listener from breaking | ||
| // the entire event dispatch chain | ||
| _ = recover() |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent panic recovery without logging could make debugging difficult. When a listener panics, the error is completely swallowed by _ = recover(). Consider at minimum logging the panic to help developers identify and fix problematic listeners, or expose panic information through a configurable error handler.
| // - Events: string, []string, event.Event, []event.Event, or any custom type | ||
| // - Listeners: functions, structs implementing listener interfaces, or closures | ||
| // | ||
| // The method automatically handles queue registration for listeners that implement ShouldQueue. |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The documentation comment references ShouldQueue as a method name, but should clarify it refers to listeners implementing the ShouldQueue interface (with ShouldQueue() method). The current phrasing "listeners that implement ShouldQueue" could be misinterpreted. Consider: "listeners that implement the ShouldQueue interface" or "listeners with ShouldQueue() returning true".
| // The method automatically handles queue registration for listeners that implement ShouldQueue. | |
| // The method automatically handles queue registration for listeners that implement the ShouldQueue interface (i.e., those with a ShouldQueue() method). |
| t.Run("QueueListenerShouldNotQueue", func(t *testing.T) { | ||
| listener := &TestQueueListener{ | ||
| ShouldQueueFunc: false, | ||
| } | ||
|
|
||
| // Should call the listener directly when not queued | ||
| result := app.callListener(listener, "test", nil) | ||
| assert.Nil(t, result) | ||
| }) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for queue listeners with ShouldQueue() returning true. The tests only cover the case where ShouldQueue() returns false (lines 421-429, 431-439). Tests should verify that when ShouldQueue() returns true, the listener is properly dispatched to the queue system rather than executed synchronously. This is a critical path for the queue integration feature.
hwbrzzl
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, amazing PR, left some questions. PTAL.
| type Listener interface { | ||
| // Signature returns the unique identifier for the listener. | ||
| Signature() string | ||
| type EventListener interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can it be merged into EventQueueListener? Don't find it used elsewhere.
| Handle(event any, args ...any) error | ||
| } | ||
|
|
||
| type EventQueueListener interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Event prefix is duplicated with the package name.
| type EventQueueListener interface { | |
| type QueueListener interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make:listener should create a listener according to the new interface. It can be implemented in this PR or another.
| Queue string | ||
| } | ||
|
|
||
| type QueueListener interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why add this function instead of using Listener directly?
| // Listen registers an event listener with the dispatcher. | ||
| // events can be: string, []string, Event, []Event, or any other type | ||
| // listener can be: function, class, or any callable | ||
| Listen(events any, listener ...any) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new Listen function can only support the new listener EventQueueListener for now. It doesn't need to support the old listener interface. We can notice this in the annotation, let users use the Register function for old listeners.
| fnType := fn.Type() | ||
|
|
||
| if fnType.NumIn() != 1 || !app.isEventType(fnType.In(0)) { | ||
| return errors.New("closure must accept exactly one event parameter") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add all errors to errors/list.go, we will implement i18n for the error in the future.
|
|
||
| // Dispatch task - errors are intentionally not returned as per framework pattern | ||
| // Queued jobs handle their own error recovery and reporting | ||
| _ = task.Dispatch() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error should be returned.
| // - payload: Optional payload arguments | ||
| // | ||
| // Returns: nil (queuing is asynchronous) | ||
| func (app *Application) queueListener(listener event.QueueListener, payload []event.Arg) any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| func (app *Application) queueListener(listener event.QueueListener, payload []event.Arg) any { | |
| func (app *Application) queueListener(listener event.QueueListener, payload []event.Arg) error { |
| // - args: Optional payload arguments | ||
| // | ||
| // Returns: nil (queuing is asynchronous) | ||
| func (app *Application) queueEventListener(listener event.EventQueueListener, evt any, args []event.Arg) any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| func (app *Application) queueEventListener(listener event.EventQueueListener, evt any, args []event.Arg) any { | |
| func (app *Application) queueEventListener(listener event.EventQueueListener, evt any, args []event.Arg) error { |
| // - args: Optional payload arguments | ||
| // | ||
| // Returns: The listener's return value, or nil if no value returned | ||
| func (app *Application) callListener(listener any, evt any, args []event.Arg) any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| func (app *Application) callListener(listener any, evt any, args []event.Arg) any { | |
| func (app *Application) callListener(listener any, evt any, args []event.Arg) error { |
| return app.handleClosure(fn) | ||
| } | ||
|
|
||
| return errors.New("listener is required") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users have to judge each error when listening to several events. Not sure if there is a better way to optimize the action.
📑 Description
Closes goravel/goravel#730
This PR introduces a modern, flexible event handling system with
Listen()andDispatch()methods, replacing the oldRegister()/Job()pattern. The new system supports wildcard patterns, dynamic listener registration, and seamless queue integration.Key Features
1. Flexible Event Registration with
Listen()The new
Listen()method supports multiple event formats and listener types:Comparison with old approach:
2. Direct Event Dispatching with
Dispatch()Fire events immediately and get listener responses:
Comparison with old approach:
3. Wildcard Pattern Support
Match multiple events with a single listener registration:
Real-world example:
4. Automatic Queue Integration
Queue-aware listeners are automatically dispatched to the queue system:
5. Dynamic Listener Arguments
Listeners automatically receive matched arguments:
Migration Guide
The old
Register()andJob()methods are now deprecated but still supported:Architecture Improvements
Backward Compatibility
Register()method still worksJob()method still worksEventandListenerinterfaces unchanged✅ Checks