# notify Thread-safe pub/sub notification system for Go applications. ## Features - **Thread-Safe**: All operations are safe for concurrent use - **Configurable Buffering**: Set custom buffer sizes per notifier - **Targeted & Broadcast**: Send to specific subscribers or broadcast to all - **Graceful Shutdown**: Built-in Close() for clean resource cleanup - **Idempotent Operations**: Safe to call Unsubscribe() and Close() multiple times - **Zero Dependencies**: Uses only Go standard library - **Comprehensive Tests**: 95%+ code coverage with race detector clean ## Installation ```bash go get git.haelnorr.com/h/golib/notify ``` ## Quick Start ```go package main import ( "fmt" "git.haelnorr.com/h/golib/notify" ) func main() { // Create a notifier with 50-notification buffer per subscriber n := notify.NewNotifier(50) defer n.Close() // Subscribe to receive notifications sub, err := n.Subscribe() if err != nil { panic(err) } defer sub.Unsubscribe() // Listen for notifications go func() { for notification := range sub.Listen() { fmt.Printf("[%s] %s: %s\n", notification.Level, notification.Title, notification.Message) } fmt.Println("Listener exited") }() // Send a notification n.Notify(notify.Notification{ Target: sub.ID, Level: notify.LevelSuccess, Title: "Welcome", Message: "You're now subscribed!", }) // Broadcast to all subscribers n.NotifyAll(notify.Notification{ Level: notify.LevelInfo, Title: "System Status", Message: "All systems operational", }) } ``` ## Usage ### Creating a Notifier The buffer size determines how many notifications can be queued per subscriber: ```go // Unbuffered - sends block until received n := notify.NewNotifier(0) // Small buffer - low memory, may drop if slow readers n := notify.NewNotifier(25) // Recommended - balanced approach n := notify.NewNotifier(50) // Large buffer - handles bursts well n := notify.NewNotifier(500) ``` ### Subscribing Each subscriber receives a unique ID and a buffered notification channel: ```go sub, err := n.Subscribe() if err != nil { // Handle error (e.g., notifier is closed) log.Fatal(err) } fmt.Println("Subscriber ID:", sub.ID) ``` ### Listening for Notifications Use a for-range loop to process notifications: ```go for notification := range sub.Listen() { switch notification.Level { case notify.LevelSuccess: fmt.Println("✓", notification.Message) case notify.LevelError: fmt.Println("✗", notification.Message) default: fmt.Println("ℹ", notification.Message) } } ``` ### Sending Targeted Notifications Send to a specific subscriber: ```go n.Notify(notify.Notification{ Target: sub.ID, Level: notify.LevelWarn, Title: "Account Warning", Message: "Password expires in 3 days", Details: "Please update your password", }) ``` ### Broadcasting to All Subscribers Send to all current subscribers: ```go n.NotifyAll(notify.Notification{ Level: notify.LevelInfo, Title: "Maintenance", Message: "System will restart in 5 minutes", }) ``` ### Unsubscribing Clean up when done (safe to call multiple times): ```go sub.Unsubscribe() ``` ### Graceful Shutdown Close the notifier to unsubscribe all and prevent new subscriptions: ```go n.Close() // After Close(): // - All subscribers are removed // - All notification channels are closed // - Future Subscribe() calls return error // - Notify/NotifyAll are no-ops ``` ## Notification Levels Four predefined levels are available: | Level | Constant | Use Case | |-------|----------|----------| | Success | `notify.LevelSuccess` | Successful operations | | Info | `notify.LevelInfo` | General information | | Warning | `notify.LevelWarn` | Non-critical warnings | | Error | `notify.LevelError` | Errors requiring attention | ## Advanced Usage ### Custom Action Data The `Action` field can hold any data type: ```go type UserAction struct { URL string Method string } n.Notify(notify.Notification{ Target: sub.ID, Level: notify.LevelInfo, Message: "New update available", Action: UserAction{ URL: "/updates/download", Method: "GET", }, }) // In listener: for notif := range sub.Listen() { if action, ok := notif.Action.(UserAction); ok { fmt.Printf("Action: %s %s\n", action.Method, action.URL) } } ``` ### Multiple Subscribers Create a notification hub for multiple clients: ```go n := notify.NewNotifier(100) defer n.Close() // Create 10 subscribers subscribers := make([]*notify.Subscriber, 10) for i := 0; i < 10; i++ { sub, _ := n.Subscribe() subscribers[i] = sub // Start listener for each go func(id int, s *notify.Subscriber) { for notif := range s.Listen() { log.Printf("Sub %d: %s", id, notif.Message) } }(i, sub) } // Broadcast to all n.NotifyAll(notify.Notification{ Level: notify.LevelSuccess, Message: "All subscribers active", }) ``` ### Concurrent-Safe Operations All operations are thread-safe: ```go n := notify.NewNotifier(50) // Safe to subscribe from multiple goroutines for i := 0; i < 100; i++ { go func() { sub, _ := n.Subscribe() defer sub.Unsubscribe() // ... }() } // Safe to notify from multiple goroutines for i := 0; i < 100; i++ { go func() { n.NotifyAll(notify.Notification{ Level: notify.LevelInfo, Message: "Concurrent notification", }) }() } ``` ## Best Practices ### 1. Use defer for Cleanup ```go n := notify.NewNotifier(50) defer n.Close() sub, _ := n.Subscribe() defer sub.Unsubscribe() ``` ### 2. Check Errors ```go sub, err := n.Subscribe() if err != nil { log.Printf("Subscribe failed: %v", err) return } ``` ### 3. Buffer Size Recommendations | Scenario | Buffer Size | |----------|------------| | Real-time chat | 10-25 | | General app notifications | 50-100 | | High-throughput logging | 200-500 | | Testing/debugging | 0 (unbuffered) | ### 4. Listener Goroutines Always use goroutines for listeners to prevent blocking: ```go // Good ✓ go func() { for notif := range sub.Listen() { process(notif) } }() // Bad ✗ - blocks main goroutine for notif := range sub.Listen() { process(notif) } ``` ### 5. Detect Channel Closure ```go for notification := range sub.Listen() { // Process notifications } // When this loop exits, the channel is closed // Either subscriber unsubscribed or notifier closed fmt.Println("No more notifications") ``` ## Performance - **Subscribe**: O(1) average case (random ID generation) - **Notify**: O(1) lookup + O(1) channel send (non-blocking) - **NotifyAll**: O(n) where n is number of subscribers - **Unsubscribe**: O(1) map deletion + O(1) channel close - **Close**: O(n) where n is number of subscribers ### Benchmarks Typical performance on modern hardware: - Subscribe: ~5-10µs per operation - Notify: ~1-2µs per operation - NotifyAll (10 subs): ~10-20µs - Buffer full handling: ~100ns (TryLock drop) ## Thread Safety All public methods are thread-safe: - ✅ `NewNotifier()` - Safe - ✅ `Subscribe()` - Safe, concurrent calls allowed - ✅ `Unsubscribe()` - Safe, idempotent - ✅ `Notify()` - Safe, concurrent calls allowed - ✅ `NotifyAll()` - Safe, concurrent calls allowed - ✅ `Close()` - Safe, idempotent - ✅ `Listen()` - Safe, returns read-only channel ## Testing Run tests: ```bash # Run all tests go test # With race detector go test -race # With coverage go test -cover # Verbose output go test -v ``` Current test coverage: **95.1%** ## Documentation Full API documentation available at: - [pkg.go.dev](https://pkg.go.dev/git.haelnorr.com/h/golib/notify) - Or run: `go doc -all git.haelnorr.com/h/golib/notify` ## License MIT License - see repository root for details ## Contributing See CONTRIBUTING.md in the repository root ## Related Projects Other modules in the golib collection: - `cookies` - HTTP cookie utilities - `env` - Environment variable helpers - `ezconf` - Configuration loader - `hlog` - Logging with zerolog - `hws` - HTTP web server - `jwt` - JWT token utilities