- Update the Subscribe and SubscribeFrom methods to return a cancel function, ensuring proper resource cleanup when subscriptions are no longer needed. - Modify unsubscribe logic to close channels safely, preventing potential panics from sending on closed channels. - Enhance test cases to utilize the new cancel functionality, ensuring robust handling of subscriptions in various scenarios.
124 lines
2.7 KiB
Go
124 lines
2.7 KiB
Go
package event
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/yaoapp/yao/event/types"
|
|
)
|
|
|
|
var subIDCounter atomic.Uint64
|
|
|
|
func nextSubID() string {
|
|
id := subIDCounter.Add(1)
|
|
return fmt.Sprintf("sub-%d", id)
|
|
}
|
|
|
|
// subEntry holds a dynamic subscriber registration.
|
|
type subEntry struct {
|
|
id string
|
|
pattern string
|
|
filter func(*types.Event) bool
|
|
ch chan<- *types.Event
|
|
}
|
|
|
|
// subManager manages dynamic subscribers.
|
|
type subManager struct {
|
|
mu sync.RWMutex
|
|
entries map[string]*subEntry // id -> entry
|
|
}
|
|
|
|
func newSubManager() *subManager {
|
|
return &subManager{
|
|
entries: make(map[string]*subEntry),
|
|
}
|
|
}
|
|
|
|
// subscribe adds a dynamic subscriber. Returns the subscription ID.
|
|
func (sm *subManager) subscribe(pattern string, ch chan<- *types.Event, opts ...types.FilterOption) string {
|
|
fe := &types.FilterEntry{Pattern: pattern}
|
|
for _, opt := range opts {
|
|
opt(fe)
|
|
}
|
|
|
|
id := nextSubID()
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
sm.entries[id] = &subEntry{
|
|
id: id,
|
|
pattern: pattern,
|
|
filter: fe.Filter,
|
|
ch: ch,
|
|
}
|
|
return id
|
|
}
|
|
|
|
// unsubscribe removes a subscriber by ID and closes its channel
|
|
// so that any goroutine blocked on `range ch` will unblock and exit.
|
|
func (sm *subManager) unsubscribe(id string) {
|
|
sm.mu.Lock()
|
|
entry, ok := sm.entries[id]
|
|
delete(sm.entries, id)
|
|
sm.mu.Unlock()
|
|
|
|
if ok && entry.ch != nil {
|
|
func() {
|
|
defer func() { recover() }()
|
|
close(entry.ch)
|
|
}()
|
|
}
|
|
}
|
|
|
|
// notify sends an event to all matching subscribers (non-blocking).
|
|
// Recovers from send-on-closed-channel panics that may occur if
|
|
// unsubscribe closes a channel concurrently.
|
|
func (sm *subManager) notify(ev *types.Event) {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
|
|
for _, entry := range sm.entries {
|
|
if !matchPattern(entry.pattern, ev.Type) {
|
|
continue
|
|
}
|
|
if entry.filter != nil && !entry.filter(ev) {
|
|
continue
|
|
}
|
|
func() {
|
|
defer func() { recover() }()
|
|
select {
|
|
case entry.ch <- ev:
|
|
default:
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// clear removes all subscribers and closes their channels. Used during Stop.
|
|
func (sm *subManager) clear() {
|
|
sm.mu.Lock()
|
|
old := sm.entries
|
|
sm.entries = make(map[string]*subEntry)
|
|
sm.mu.Unlock()
|
|
|
|
for _, entry := range old {
|
|
if entry.ch != nil {
|
|
func() {
|
|
defer func() { recover() }()
|
|
close(entry.ch)
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Subscribe dynamically subscribes to events matching the given pattern.
|
|
// Returns the subscription ID for later unsubscription.
|
|
// Event delivery is non-blocking: if ch is full, the event is skipped.
|
|
func Subscribe(pattern string, ch chan<- *types.Event, opts ...types.FilterOption) string {
|
|
return svc.smgr.subscribe(pattern, ch, opts...)
|
|
}
|
|
|
|
// Unsubscribe removes a dynamic subscription by ID.
|
|
func Unsubscribe(id string) {
|
|
svc.smgr.unsubscribe(id)
|
|
}
|