112 lines
3.9 KiB
Go
112 lines
3.9 KiB
Go
package channel
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// TaskTypeEventDispatch is the async task type for event dispatch.
|
|
// Reference: Chatwoot EventDispatcherJob (Sidekiq worker)
|
|
// TODO: asynq integration in P10 (async task processing)
|
|
const TaskTypeEventDispatch = "event:dispatch_async"
|
|
|
|
// EventListener is the interface that all event listeners must implement.
|
|
// Reference: Chatwoot's Wisper listener pattern — each listener subscribes to specific event names
|
|
// and runs its logic when the event is broadcast.
|
|
type EventListener interface {
|
|
// Name returns the unique identifier for this listener.
|
|
Name() string
|
|
|
|
// OnEvent processes the given event.
|
|
// Implementations should be idempotent and handle errors gracefully.
|
|
OnEvent(ctx context.Context, event *ChannelEvent) error
|
|
}
|
|
|
|
// Dispatcher is the central event dispatch hub.
|
|
// Reference: Chatwoot Dispatcher + Listener pattern — replaces both SyncDispatcher and AsyncDispatcher.
|
|
//
|
|
// Design:
|
|
// - Dispatch calls registered listeners synchronously.
|
|
// - DispatchAsync will be implemented via asynq in P10.
|
|
// - Listeners are registered once (startup) and called for every matching event type.
|
|
type Dispatcher struct {
|
|
mu sync.RWMutex
|
|
listeners map[string]EventListener // name → listener (prevents duplicates)
|
|
}
|
|
|
|
// NewDispatcher creates a Dispatcher with empty listener registry.
|
|
func NewDispatcher() *Dispatcher {
|
|
return &Dispatcher{
|
|
listeners: make(map[string]EventListener),
|
|
}
|
|
}
|
|
|
|
// Register adds a listener. Duplicate names overwrite.
|
|
func (d *Dispatcher) Register(listener EventListener) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.listeners[listener.Name()] = listener
|
|
applogger.L().Infof("registered event listener: %s", listener.Name())
|
|
}
|
|
|
|
// Unregister removes a listener by name.
|
|
func (d *Dispatcher) Unregister(name string) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
delete(d.listeners, name)
|
|
applogger.L().Infof("unregistered event listener: %s", name)
|
|
}
|
|
|
|
// Dispatch broadcasts an event to all registered listeners synchronously.
|
|
// Reference: Chatwoot SyncDispatcher — Wisper.broadcast(event_name, *args)
|
|
// Errors are logged but do not stop propagation to other listeners.
|
|
func (d *Dispatcher) Dispatch(ctx context.Context, event *ChannelEvent) error {
|
|
d.mu.RLock()
|
|
listeners := make([]EventListener, 0, len(d.listeners))
|
|
for _, l := range d.listeners {
|
|
listeners = append(listeners, l)
|
|
}
|
|
d.mu.RUnlock()
|
|
|
|
applogger.L().Infof("dispatching event %s to %d listeners", event.Type, len(listeners))
|
|
|
|
var firstErr error
|
|
for _, listener := range listeners {
|
|
if err := listener.OnEvent(ctx, event); err != nil {
|
|
applogger.L().Errorf("listener %s failed on event %s: %v", listener.Name(), event.Type, err)
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
// Continue dispatching to remaining listeners (Chatwoot Wisper pattern)
|
|
}
|
|
}
|
|
return firstErr
|
|
}
|
|
|
|
// DispatchAsync enqueues an event for async processing.
|
|
// TODO: Implement with asynq in P10. Currently falls back to synchronous dispatch.
|
|
func (d *Dispatcher) DispatchAsync(ctx context.Context, event *ChannelEvent) error {
|
|
// For now, fall back to synchronous dispatch.
|
|
// P10 will implement asynq task enqueue:
|
|
// payload, _ := json.Marshal(event)
|
|
// task := asynq.NewTask(TaskTypeEventDispatch, payload)
|
|
// _, err = d.taskClient.Enqueue(task, asynq.Queue("events"), asynq.MaxRetry(3))
|
|
applogger.L().Infof("async dispatch (sync fallback) event %s", event.Type)
|
|
return d.Dispatch(ctx, event)
|
|
}
|
|
|
|
// HandleAsyncTask processes an asynq task for async event dispatch.
|
|
// TODO: Implement with asynq.Server handler in P10.
|
|
func HandleAsyncTask(ctx context.Context, payload []byte) error {
|
|
var event ChannelEvent
|
|
if err := json.Unmarshal(payload, &event); err != nil {
|
|
return fmt.Errorf("failed to unmarshal event payload: %w", err)
|
|
}
|
|
applogger.L().Infof("handling async task for event %s", event.Type)
|
|
// TODO: Call dispatcher.Dispatch(ctx, &event) after asynq server setup in P10
|
|
return nil
|
|
} |