package aether import ( "context" "strings" "sync" ) // SubscriptionFilter defines criteria for filtering events in a subscription. // Multiple filters are combined with AND logic - an event must match all // non-empty filter criteria to be delivered. // // # Event Type Filtering // // EventTypes specifies which event types to receive. If empty, all event types // are delivered. Otherwise, only events with a matching EventType are delivered. // // filter := SubscriptionFilter{ // EventTypes: []string{"OrderPlaced", "OrderShipped"}, // } // // # Actor Pattern Filtering // // ActorPattern specifies a pattern to match against actor IDs. Patterns support // two matching modes: // // Prefix matching with "*" suffix: // // "order-*" // matches "order-123", "order-456", etc. // "user-*" // matches "user-abc", "user-xyz", etc. // // Exact matching (no wildcard): // // "order-123" // matches only "order-123" // // An empty ActorPattern matches all actor IDs. // // # Combining Filters // // When both EventTypes and ActorPattern are specified, an event must match // both criteria (AND logic): // // filter := SubscriptionFilter{ // EventTypes: []string{"OrderPlaced"}, // ActorPattern: "order-*", // } // // Only delivers OrderPlaced events from actors starting with "order-" type SubscriptionFilter struct { // EventTypes limits delivery to events with matching EventType. // Empty slice means all event types are accepted. EventTypes []string // ActorPattern matches against event ActorID. // Supports prefix matching with "*" suffix (e.g., "order-*"). // Empty string matches all actor IDs. ActorPattern string } // Matches returns true if the event passes all filter criteria. // An empty filter (no event types, no actor pattern) matches all events. func (f SubscriptionFilter) Matches(event *Event) bool { if event == nil { return false } // Check event type filter if len(f.EventTypes) > 0 { found := false for _, et := range f.EventTypes { if event.EventType == et { found = true break } } if !found { return false } } // Check actor pattern filter if f.ActorPattern != "" { if strings.HasSuffix(f.ActorPattern, "*") { // Prefix matching prefix := strings.TrimSuffix(f.ActorPattern, "*") if !strings.HasPrefix(event.ActorID, prefix) { return false } } else { // Exact matching if event.ActorID != f.ActorPattern { return false } } } return true } // EventBroadcaster defines the interface for publishing and subscribing to events type EventBroadcaster interface { Subscribe(namespaceID string) <-chan *Event SubscribeWithFilter(namespaceID string, filter SubscriptionFilter) <-chan *Event Unsubscribe(namespaceID string, ch <-chan *Event) Publish(namespaceID string, event *Event) Stop() SubscriberCount(namespaceID string) int } // filteredSubscriber holds a subscriber channel and its filter type filteredSubscriber struct { ch chan *Event filter SubscriptionFilter } // EventBus broadcasts events to multiple subscribers within a namespace type EventBus struct { subscribers map[string][]filteredSubscriber // namespaceID -> filtered subscribers mutex sync.RWMutex ctx context.Context cancel context.CancelFunc } // NewEventBus creates a new event bus func NewEventBus() *EventBus { ctx, cancel := context.WithCancel(context.Background()) return &EventBus{ subscribers: make(map[string][]filteredSubscriber), ctx: ctx, cancel: cancel, } } // Subscribe creates a new subscription channel for a namespace. // All events published to the namespace will be delivered. func (eb *EventBus) Subscribe(namespaceID string) <-chan *Event { return eb.SubscribeWithFilter(namespaceID, SubscriptionFilter{}) } // SubscribeWithFilter creates a new subscription channel for a namespace with filtering. // Only events matching the filter criteria will be delivered. func (eb *EventBus) SubscribeWithFilter(namespaceID string, filter SubscriptionFilter) <-chan *Event { eb.mutex.Lock() defer eb.mutex.Unlock() // Create buffered channel to prevent blocking publishers ch := make(chan *Event, 100) sub := filteredSubscriber{ ch: ch, filter: filter, } eb.subscribers[namespaceID] = append(eb.subscribers[namespaceID], sub) return ch } // Unsubscribe removes a subscription channel func (eb *EventBus) Unsubscribe(namespaceID string, ch <-chan *Event) { eb.mutex.Lock() defer eb.mutex.Unlock() subs := eb.subscribers[namespaceID] for i, subscriber := range subs { if subscriber.ch == ch { // Remove subscriber from slice eb.subscribers[namespaceID] = append(subs[:i], subs[i+1:]...) close(subscriber.ch) break } } // Clean up empty namespace entries if len(eb.subscribers[namespaceID]) == 0 { delete(eb.subscribers, namespaceID) } } // Publish sends an event to all subscribers of a namespace whose filters match func (eb *EventBus) Publish(namespaceID string, event *Event) { eb.mutex.RLock() defer eb.mutex.RUnlock() subscribers := eb.subscribers[namespaceID] for _, sub := range subscribers { // Apply filter before delivering if !sub.filter.Matches(event) { continue } select { case sub.ch <- event: // Event delivered default: // Channel full, skip this subscriber (non-blocking) } } } // Stop closes the event bus func (eb *EventBus) Stop() { eb.mutex.Lock() defer eb.mutex.Unlock() eb.cancel() // Close all subscriber channels for _, subs := range eb.subscribers { for _, sub := range subs { close(sub.ch) } } eb.subscribers = make(map[string][]filteredSubscriber) } // SubscriberCount returns the number of subscribers for a namespace func (eb *EventBus) SubscriberCount(namespaceID string) int { eb.mutex.RLock() defer eb.mutex.RUnlock() return len(eb.subscribers[namespaceID]) }