package channel import ( "context" "encoding/json" "fmt" "sync" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/worker" applogger "github.com/gochat/gochat/pkg/logger" ) // TaskTypeEventDispatch is the async task type for event dispatch. // Reference: Chatwoot EventDispatcherJob (Sidekiq worker) 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) worker *worker.WorkerPool } // NewDispatcher creates a Dispatcher with empty listener registry. func NewDispatcher(workers ...*worker.WorkerPool) *Dispatcher { d := &Dispatcher{ listeners: make(map[string]EventListener), } if len(workers) > 0 { d.SetWorkerPool(workers[0]) } return d } // SetWorkerPool wires DispatchAsync to the durable background job worker. func (d *Dispatcher) SetWorkerPool(wp *worker.WorkerPool) { d.mu.Lock() d.worker = wp d.mu.Unlock() if wp != nil { wp.Register(TaskTypeEventDispatch, func(ctx context.Context, job *model.BackgroundJob) error { return d.handleAsyncJob(ctx, job.Payload) }) } } // 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. // When no durable worker is configured, it preserves the legacy synchronous fallback. func (d *Dispatcher) DispatchAsync(ctx context.Context, event *ChannelEvent) error { wp := d.workerPool() if wp != nil { _, err := wp.Enqueue(ctx, TaskTypeEventDispatch, event, worker.WithQueue("events"), worker.WithMaxAttempts(3)) if err != nil { return err } applogger.L().Infof("async dispatch enqueued event %s", event.Type) return nil } applogger.L().Infof("async dispatch (sync fallback) event %s", event.Type) return d.Dispatch(ctx, event) } func (d *Dispatcher) workerPool() *worker.WorkerPool { d.mu.RLock() defer d.mu.RUnlock() return d.worker } func (d *Dispatcher) handleAsyncJob(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) } return d.Dispatch(ctx, &event) } // HandleAsyncTask processes a durable async event payload for compatibility with // older callers that only need payload validation. 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) return nil }