added the notify module

This commit is contained in:
2026-01-24 20:35:40 +11:00
parent 0c3d4ef095
commit 65e8bd07e1
14 changed files with 2728 additions and 0 deletions

397
notify/README.md Normal file
View File

@@ -0,0 +1,397 @@
# 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