8.3 KiB
8.3 KiB
WebSocket REQ Handling Comparison: Khatru vs Next.orly.dev
Overview
This document compares how two Nostr relay implementations handle WebSocket connections and REQ (subscription) messages:
- Khatru - A popular Go-based Nostr relay library by fiatjaf
- Next.orly.dev - A custom relay implementation with advanced features
Architecture Comparison
Khatru Architecture
- Monolithic approach: Single large
HandleWebsocketmethod (~380 lines) processes all message types - Inline processing: REQ handling is embedded within the main websocket handler
- Hook-based extensibility: Uses function slices for customizable behavior
- Simple structure: WebSocket struct with basic fields and mutex for thread safety
Next.orly.dev Architecture
- Modular approach: Separate methods for each message type (
HandleReq,HandleEvent, etc.) - Layered processing: Message identification → envelope parsing → type-specific handling
- Publisher-subscriber system: Dedicated infrastructure for subscription management
- Rich context: Listener struct with detailed state tracking and metrics
Connection Establishment
Khatru
// Simple websocket upgrade
conn, err := rl.upgrader.Upgrade(w, r, nil)
ws := &WebSocket{
conn: conn,
Request: r,
Challenge: hex.EncodeToString(challenge),
negentropySessions: xsync.NewMapOf[string, *NegentropySession](),
}
Next.orly.dev
// More sophisticated setup with IP whitelisting
conn, err = websocket.Accept(w, r, &websocket.AcceptOptions{OriginPatterns: []string{"*"}})
listener := &Listener{
ctx: ctx,
Server: s,
conn: conn,
remote: remote,
req: r,
}
// Immediate AUTH challenge if ACLs are configured
Key Differences:
- Next.orly.dev includes IP whitelisting and immediate authentication challenges
- Khatru uses fasthttp/websocket library vs next.orly.dev using coder/websocket
- Next.orly.dev has more detailed connection state tracking
Message Processing
Khatru
- Uses
nostr.MessageParserfor sequential parsing - Switch statement on envelope type within goroutine
- Direct processing without intermediate validation layers
Next.orly.dev
- Custom envelope identification system (
envelopes.Identify) - Separate validation and processing phases
- Extensive logging and error handling at each step
REQ Message Handling
Khatru REQ Processing
case *nostr.ReqEnvelope:
eose := sync.WaitGroup{}
eose.Add(len(env.Filters))
// Handle each filter separately
for _, filter := range env.Filters {
err := srl.handleRequest(reqCtx, env.SubscriptionID, &eose, ws, filter)
if err != nil {
// Fail everything if any filter is rejected
ws.WriteJSON(nostr.ClosedEnvelope{SubscriptionID: env.SubscriptionID, Reason: reason})
return
} else {
rl.addListener(ws, env.SubscriptionID, srl, filter, cancelReqCtx)
}
}
go func() {
eose.Wait()
ws.WriteJSON(nostr.EOSEEnvelope(env.SubscriptionID))
}()
Next.orly.dev REQ Processing
// Comprehensive ACL and authentication checks first
accessLevel := acl.Registry.GetAccessLevel(l.authedPubkey.Load(), l.remote)
switch accessLevel {
case "none":
return // Send auth-required response
}
// Process all filters and collect events
for _, f := range *env.Filters {
filterEvents, err = l.QueryEvents(queryCtx, f)
allEvents = append(allEvents, filterEvents...)
}
// Apply privacy and privilege checks
// Send all historical events
// Set up ongoing subscription only if needed
Key Architectural Differences
1. Filter Processing Strategy
Khatru:
- Processes each filter independently and concurrently
- Uses WaitGroup to coordinate EOSE across all filters
- Immediately sets up listeners for ongoing subscriptions
- Fails entire subscription if any filter is rejected
Next.orly.dev:
- Processes all filters sequentially in a single context
- Collects all events before applying access control
- Only sets up subscriptions for filters that need ongoing updates
- Gracefully handles individual filter failures
2. Access Control Integration
Khatru:
- Basic NIP-42 authentication support
- Hook-based authorization via
RejectFilterfunctions - Limited built-in access control features
Next.orly.dev:
- Comprehensive ACL system with multiple access levels
- Built-in support for private events with npub authorization
- Privileged event filtering based on pubkey and p-tags
- Granular permission checking at multiple stages
3. Subscription Management
Khatru:
// Simple listener registration
type listenerSpec struct {
filter nostr.Filter
cancel context.CancelCauseFunc
subRelay *Relay
}
rl.addListener(ws, subscriptionID, relay, filter, cancel)
Next.orly.dev:
// Publisher-subscriber system with rich metadata
type W struct {
Conn *websocket.Conn
remote string
Id string
Receiver event.C
Filters *filter.S
AuthedPubkey []byte
}
l.publishers.Receive(&W{...})
4. Performance Optimizations
Khatru:
- Concurrent filter processing
- Immediate streaming of events as they're found
- Memory-efficient with direct event streaming
Next.orly.dev:
- Batch processing with deduplication
- Memory management with explicit
ev.Free()calls - Smart subscription cancellation for ID-only queries
- Event result caching and seen-tracking
5. Error Handling & Observability
Khatru:
- Basic error logging
- Simple connection state management
- Limited metrics and observability
Next.orly.dev:
- Comprehensive error handling with context preservation
- Detailed logging at each processing stage
- Built-in metrics (message count, REQ count, event count)
- Graceful degradation on individual component failures
Memory Management
Khatru
- Relies on Go's garbage collector
- Simple WebSocket struct with minimal state
- Uses sync.Map for thread-safe operations
Next.orly.dev
- Explicit memory management with
ev.Free()calls - Resource pooling and reuse patterns
- Detailed tracking of connection resources
Concurrency Models
Khatru
- Per-connection goroutine for message reading
- Additional goroutines for each message processing
- WaitGroup coordination for multi-filter EOSE
Next.orly.dev
- Per-connection goroutine with single-threaded message processing
- Publisher-subscriber system handles concurrent event distribution
- Context-based cancellation throughout
Trade-offs Analysis
Khatru Advantages
- Simplicity: Easier to understand and modify
- Performance: Lower latency due to concurrent processing
- Flexibility: Hook-based architecture allows extensive customization
- Streaming: Events sent as soon as they're found
Khatru Disadvantages
- Monolithic: Large methods harder to maintain
- Limited ACL: Basic authentication and authorization
- Error handling: Less graceful failure recovery
- Resource usage: No explicit memory management
Next.orly.dev Advantages
- Security: Comprehensive ACL and privacy features
- Observability: Extensive logging and metrics
- Resource management: Explicit memory and connection lifecycle management
- Modularity: Easier to test and extend individual components
- Robustness: Graceful handling of edge cases and failures
Next.orly.dev Disadvantages
- Complexity: Higher cognitive overhead and learning curve
- Latency: Sequential processing may be slower for some use cases
- Resource overhead: More memory usage due to batching and state tracking
- Coupling: Tighter integration between components
Conclusion
Both implementations represent different philosophies:
- Khatru prioritizes simplicity, performance, and extensibility through a hook-based architecture
- Next.orly.dev prioritizes security, observability, and robustness through comprehensive built-in features
The choice between them depends on specific requirements:
- Choose Khatru for high-performance relays with custom business logic
- Choose Next.orly.dev for production relays requiring comprehensive access control and monitoring
Both approaches demonstrate mature understanding of Nostr protocol requirements while making different trade-offs in complexity vs. features.