Files
golib/notify/README.md
2026-01-24 20:35:40 +11:00

398 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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