Skip to content
73 changes: 54 additions & 19 deletions contracts/event/events.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,75 @@
package event

type Instance interface {
// Register event listeners to the application.
Register(map[Event][]Listener)
// Job create a new event task.
Job(event Event, args []Arg) Task
// GetEvents gets all registered events.
GetEvents() map[Event][]Listener
type Arg struct {
Type string
Value any
}

type Event interface {
// Handle the event.
Handle(args []Arg) ([]Arg, error)
}

type Listener interface {
// Signature returns the unique identifier for the listener.
Signature() string
type EventListener interface {
Copy link
Contributor

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 the event.
Handle(event any, args ...any) error
}

type EventQueueListener interface {
Copy link
Contributor

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.

Suggested change
type EventQueueListener interface {
type QueueListener interface {

Copy link
Contributor

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 configure the event queue options.
Queue(args ...any) Queue
// Handle the event.
Handle(args ...any) error

EventListener
Signature
ShouldQueue
}

type Task interface {
// Dispatch an event and call the listeners.
Dispatch() error
type Instance interface {
// Dispatch fires an event and calls the listeners.
Dispatch(event any, payload ...[]Arg) []any
Copy link
Contributor

Choose a reason for hiding this comment

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

[]any should be []error, right? How about adding a Result interface?

type Result interface {
  Error() error // use errors.Join to zip the errors
  Errors() []error
  Failed() bool
}

// GetEvents gets all registered events.
GetEvents() map[Event][]Listener
// Job create a new event task.
// Deprecated: Use Dispatch instead. Job will be removed in a future version.
Job(event Event, args []Arg) Task
// 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
Copy link
Contributor

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.

// Register event listeners to the application.
Register(map[Event][]Listener)
Copy link
Contributor

Choose a reason for hiding this comment

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

Add the DEPRECATED tag for this function.

}

type Arg struct {
Value any
Type string
type Listener interface {
// Handle the event.
Handle(args ...any) error
// Queue configure the event queue options.
Queue(args ...any) Queue
// Signature returns the unique identifier for the listener.
Signature() string
}

type Queue struct {
Connection string
Queue string
Enable bool
Queue string
}

type QueueListener interface {
Copy link
Contributor

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?

Listener
Signature
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

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

The QueueListener interface embeds Listener, Signature, and ShouldQueue. However, Listener already includes a Signature() string method at line 49, making the separate Signature embedding at line 60 redundant. This creates ambiguity and could cause confusion.

Consider removing the redundant Signature embedding:

type QueueListener interface {
    Listener
    ShouldQueue
}
Suggested change
Signature

Copilot uses AI. Check for mistakes.
ShouldQueue
}

type ShouldQueue interface {
ShouldQueue() bool
}

type Signature interface {
Signature() string
}

type Task interface {
// Dispatch an event and call the listeners.
Dispatch() error
}
55 changes: 39 additions & 16 deletions event/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,58 @@ package event

import (
"slices"
"sync"

"github.com/goravel/framework/contracts/event"
"github.com/goravel/framework/contracts/queue"
)

type Application struct {
events map[event.Event][]event.Listener
queue queue.Queue
listeners map[string][]any
mu sync.RWMutex
wildcards map[string][]any
wildcardsCache sync.Map
events map[event.Event][]event.Listener
queue queue.Queue
}

func NewApplication(queue queue.Queue) *Application {
return &Application{
queue: queue,
listeners: make(map[string][]any),
mu: sync.RWMutex{},
Copy link

Copilot AI Dec 1, 2025

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.

Suggested change
mu: sync.RWMutex{},

Copilot uses AI. Check for mistakes.
wildcards: make(map[string][]any),
wildcardsCache: sync.Map{},
queue: queue,
}
}

// GetEvents returns all registered events and their listeners.
func (app *Application) GetEvents() map[event.Event][]event.Listener {
return app.events
Copy link

Copilot AI Nov 29, 2025

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:

  1. Protecting the access with app.mu.RLock() and returning a copy of the map, or
  2. Documenting that the returned map should not be modified by callers.
Suggested change
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

Copilot uses AI. Check for mistakes.
}

// Job returns a new Task for the given event and arguments.
// Deprecated: Use Dispatch instead. Job will be removed in a future version.
// The new Dispatch method fires events immediately and returns listener responses.
//
// Migration example:
//
// // Old way
// task := app.Job(event, args)
// task.Dispatch()
//
// // New way
// responses := app.Dispatch(event, args)
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)
}
Comment on lines +47 to +54
Copy link

Copilot AI Nov 29, 2025

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.

Copilot uses AI. Check for mistakes.

// Register registers events and their listeners.
func (app *Application) Register(events map[event.Event][]event.Listener) {
var (
jobs []queue.Job
Expand All @@ -40,16 +76,3 @@ func (app *Application) Register(events map[event.Event][]event.Listener) {

app.queue.Register(jobs)
}

func (app *Application) GetEvents() map[event.Event][]event.Listener {
return app.events
}

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)
}
Loading
Loading