Add namespace event filtering support
All checks were successful
CI / build (pull_request) Successful in 21s
All checks were successful
CI / build (pull_request) Successful in 21s
Add SubscriptionFilter type and SubscribeWithFilter method to enable filtering events by type and actor pattern within namespace subscriptions. - SubscriptionFilter supports event type filtering (e.g., only "OrderPlaced") - SubscriptionFilter supports actor ID prefix patterns (e.g., "order-*") - Filters are combined with AND logic - NATSEventBus uses NATS subjects for server-side filtering when possible - Comprehensive test coverage for filtering functionality Closes #21 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
140
eventbus.go
140
eventbus.go
@@ -2,21 +2,119 @@ 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][]chan *Event // namespaceID -> channels
|
||||
subscribers map[string][]filteredSubscriber // namespaceID -> filtered subscribers
|
||||
mutex sync.RWMutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@@ -26,20 +124,31 @@ type EventBus struct {
|
||||
func NewEventBus() *EventBus {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &EventBus{
|
||||
subscribers: make(map[string][]chan *Event),
|
||||
subscribers: make(map[string][]filteredSubscriber),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe creates a new subscription channel for a namespace
|
||||
// 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)
|
||||
eb.subscribers[namespaceID] = append(eb.subscribers[namespaceID], ch)
|
||||
sub := filteredSubscriber{
|
||||
ch: ch,
|
||||
filter: filter,
|
||||
}
|
||||
eb.subscribers[namespaceID] = append(eb.subscribers[namespaceID], sub)
|
||||
|
||||
return ch
|
||||
}
|
||||
@@ -51,10 +160,10 @@ func (eb *EventBus) Unsubscribe(namespaceID string, ch <-chan *Event) {
|
||||
|
||||
subs := eb.subscribers[namespaceID]
|
||||
for i, subscriber := range subs {
|
||||
if subscriber == ch {
|
||||
// Remove channel from slice
|
||||
if subscriber.ch == ch {
|
||||
// Remove subscriber from slice
|
||||
eb.subscribers[namespaceID] = append(subs[:i], subs[i+1:]...)
|
||||
close(subscriber)
|
||||
close(subscriber.ch)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -65,15 +174,20 @@ func (eb *EventBus) Unsubscribe(namespaceID string, ch <-chan *Event) {
|
||||
}
|
||||
}
|
||||
|
||||
// Publish sends an event to all subscribers of a namespace
|
||||
// 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 _, ch := range subscribers {
|
||||
for _, sub := range subscribers {
|
||||
// Apply filter before delivering
|
||||
if !sub.filter.Matches(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- event:
|
||||
case sub.ch <- event:
|
||||
// Event delivered
|
||||
default:
|
||||
// Channel full, skip this subscriber (non-blocking)
|
||||
@@ -90,12 +204,12 @@ func (eb *EventBus) Stop() {
|
||||
|
||||
// Close all subscriber channels
|
||||
for _, subs := range eb.subscribers {
|
||||
for _, ch := range subs {
|
||||
close(ch)
|
||||
for _, sub := range subs {
|
||||
close(sub.ch)
|
||||
}
|
||||
}
|
||||
|
||||
eb.subscribers = make(map[string][]chan *Event)
|
||||
eb.subscribers = make(map[string][]filteredSubscriber)
|
||||
}
|
||||
|
||||
// SubscriberCount returns the number of subscribers for a namespace
|
||||
|
||||
Reference in New Issue
Block a user