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 }