Files
gochat/internal/channel/dispatcher.go
T
2026-06-04 15:44:48 +08:00

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
}