Files
next.orly.dev/.claude/skills/nostr-websocket/SKILL.md
mleku d604341a27
Some checks failed
Go / build (push) Has been cancelled
Go / release (push) Has been cancelled
Add comprehensive documentation for CLAUDE and Nostr WebSocket skills
- Introduced CLAUDE.md to provide guidance for working with the Claude Code repository, including project overview, build commands, testing guidelines, and performance considerations.
- Added INDEX.md for a structured overview of the strfry WebSocket implementation analysis, detailing document contents and usage.
- Created SKILL.md for the nostr-websocket skill, covering WebSocket protocol fundamentals, connection management, and performance optimization techniques.
- Included multiple reference documents for Go, C++, and Rust implementations of WebSocket patterns, enhancing the knowledge base for developers.
- Updated deployment and build documentation to reflect new multi-platform capabilities and pure Go build processes.
- Bumped version to reflect the addition of extensive documentation and resources for developers working with Nostr relays and WebSocket connections.
2025-11-06 16:18:09 +00:00

978 lines
26 KiB
Markdown

---
name: nostr-websocket
description: This skill should be used when implementing, debugging, or discussing WebSocket connections for Nostr relays. Provides comprehensive knowledge of RFC 6455 WebSocket protocol, production-ready implementation patterns in Go (khatru), C++ (strfry), and Rust (nostr-rs-relay), including connection lifecycle, message framing, subscription management, and performance optimization techniques specific to Nostr relay operations.
---
# Nostr WebSocket Programming
## Overview
Implement robust, high-performance WebSocket connections for Nostr relays following RFC 6455 specifications and battle-tested production patterns. This skill provides comprehensive guidance on WebSocket protocol fundamentals, connection management, message handling, and language-specific implementation strategies using proven codebases.
## Core WebSocket Protocol (RFC 6455)
### Connection Upgrade Handshake
The WebSocket connection begins with an HTTP upgrade request:
**Client Request Headers:**
- `Upgrade: websocket` - Required
- `Connection: Upgrade` - Required
- `Sec-WebSocket-Key` - 16-byte random value, base64-encoded
- `Sec-WebSocket-Version: 13` - Required
- `Origin` - Required for browser clients (security)
**Server Response (HTTP 101):**
- `HTTP/1.1 101 Switching Protocols`
- `Upgrade: websocket`
- `Connection: Upgrade`
- `Sec-WebSocket-Accept` - SHA-1(client_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"), base64-encoded
**Security validation:** Always verify the `Sec-WebSocket-Accept` value matches expected computation. Reject connections with missing or incorrect values.
### Frame Structure
WebSocket frames use binary encoding with variable-length fields:
**Header (minimum 2 bytes):**
- **FIN bit** (1 bit) - Final fragment indicator
- **RSV1-3** (3 bits) - Reserved for extensions (must be 0)
- **Opcode** (4 bits) - Frame type identifier
- **MASK bit** (1 bit) - Payload masking indicator
- **Payload length** (7, 7+16, or 7+64 bits) - Variable encoding
**Payload length encoding:**
- 0-125: Direct 7-bit value
- 126: Next 16 bits contain length
- 127: Next 64 bits contain length
### Frame Opcodes
**Data Frames:**
- `0x0` - Continuation frame
- `0x1` - Text frame (UTF-8)
- `0x2` - Binary frame
**Control Frames:**
- `0x8` - Connection close
- `0x9` - Ping
- `0xA` - Pong
**Control frame constraints:**
- Maximum 125-byte payload
- Cannot be fragmented
- Must be processed immediately
### Masking Requirements
**Critical security requirement:**
- Client-to-server frames MUST be masked
- Server-to-client frames MUST NOT be masked
- Masking uses XOR with 4-byte random key
- Prevents cache poisoning and intermediary attacks
**Masking algorithm:**
```
transformed[i] = original[i] XOR masking_key[i MOD 4]
```
### Ping/Pong Keep-Alive
**Purpose:** Detect broken connections and maintain NAT traversal
**Pattern:**
1. Either endpoint sends Ping (0x9) with optional payload
2. Recipient responds with Pong (0xA) containing identical payload
3. Implement timeouts to detect unresponsive connections
**Nostr relay recommendations:**
- Send pings every 30-60 seconds
- Timeout after 60-120 seconds without pong response
- Close connections exceeding timeout threshold
### Close Handshake
**Initiation:** Either peer sends Close frame (0x8)
**Close frame structure:**
- Optional 2-byte status code
- Optional UTF-8 reason string
**Common status codes:**
- `1000` - Normal closure
- `1001` - Going away (server shutdown/navigation)
- `1002` - Protocol error
- `1003` - Unsupported data type
- `1006` - Abnormal closure (no close frame)
- `1011` - Server error
**Proper shutdown sequence:**
1. Initiator sends Close frame
2. Recipient responds with Close frame
3. Both close TCP connection
## Nostr Relay WebSocket Architecture
### Message Flow Overview
```
Client Relay
| |
|--- HTTP Upgrade ------->|
|<-- 101 Switching -------|
| |
|--- ["EVENT", {...}] --->| (Validate, store, broadcast)
|<-- ["OK", id, ...] -----|
| |
|--- ["REQ", id, {...}]-->| (Query + subscribe)
|<-- ["EVENT", id, {...}]-| (Stored events)
|<-- ["EOSE", id] --------| (End of stored)
|<-- ["EVENT", id, {...}]-| (Real-time events)
| |
|--- ["CLOSE", id] ------>| (Unsubscribe)
| |
|--- Close Frame -------->|
|<-- Close Frame ---------|
```
### Critical Concurrency Considerations
**Write concurrency:** WebSocket libraries panic/error on concurrent writes. Always protect writes with:
- Mutex locks (Go, C++)
- Single-writer goroutine/thread pattern
- Message queue with dedicated sender
**Read concurrency:** Concurrent reads generally allowed but not useful - implement single reader loop per connection.
**Subscription management:** Concurrent access to subscription maps requires synchronization or lock-free data structures.
## Language-Specific Implementation Patterns
### Go Implementation (khatru-style)
**Recommended library:** `github.com/fasthttp/websocket`
**Connection structure:**
```go
type WebSocket struct {
conn *websocket.Conn
mutex sync.Mutex // Protects writes
Request *http.Request // Original HTTP request
Context context.Context // Cancellation context
cancel context.CancelFunc
// NIP-42 authentication
Challenge string
AuthedPublicKey string
// Concurrent session management
negentropySessions *xsync.MapOf[string, *NegentropySession]
}
// Thread-safe write
func (ws *WebSocket) WriteJSON(v any) error {
ws.mutex.Lock()
defer ws.mutex.Unlock()
return ws.conn.WriteJSON(v)
}
```
**Lifecycle pattern (dual goroutines):**
```go
// Read goroutine
go func() {
defer cleanup()
ws.conn.SetReadLimit(maxMessageSize)
ws.conn.SetReadDeadline(time.Now().Add(pongWait))
ws.conn.SetPongHandler(func(string) error {
ws.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
typ, msg, err := ws.conn.ReadMessage()
if err != nil {
return // Connection closed
}
if typ == websocket.PingMessage {
ws.WriteMessage(websocket.PongMessage, nil)
continue
}
// Parse and handle message in separate goroutine
go handleMessage(msg)
}
}()
// Write/ping goroutine
go func() {
defer cleanup()
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}()
```
**Key patterns:**
- **Mutex-protected writes** - Prevent concurrent write panics
- **Context-based lifecycle** - Clean cancellation hierarchy
- **Swap-delete for subscriptions** - O(1) removal from listener arrays
- **Zero-copy string conversion** - `unsafe.String()` for message parsing
- **Goroutine-per-message** - Sequential parsing, concurrent handling
- **Hook-based extensibility** - Plugin architecture without core modifications
**Configuration constants:**
```go
WriteWait: 10 * time.Second // Write timeout
PongWait: 60 * time.Second // Pong timeout
PingPeriod: 30 * time.Second // Ping interval (< PongWait)
MaxMessageSize: 512000 // 512 KB limit
```
**Subscription management:**
```go
type listenerSpec struct {
id string
cancel context.CancelCauseFunc
index int
subrelay *Relay
}
// Efficient removal with swap-delete
func (rl *Relay) removeListenerId(ws *WebSocket, id string) {
rl.clientsMutex.Lock()
defer rl.clientsMutex.Unlock()
if specs, ok := rl.clients[ws]; ok {
for i := len(specs) - 1; i >= 0; i-- {
if specs[i].id == id {
specs[i].cancel(ErrSubscriptionClosedByClient)
specs[i] = specs[len(specs)-1]
specs = specs[:len(specs)-1]
rl.clients[ws] = specs
break
}
}
}
}
```
For detailed khatru implementation examples, see [references/khatru_implementation.md](references/khatru_implementation.md).
### C++ Implementation (strfry-style)
**Recommended library:** Custom fork of `uWebSockets` with epoll
**Architecture highlights:**
- Single-threaded I/O using epoll for connection multiplexing
- Thread pool architecture: 6 specialized pools (WebSocket, Ingester, Writer, ReqWorker, ReqMonitor, Negentropy)
- "Shared nothing" message-passing design eliminates lock contention
- Deterministic thread assignment: `connId % numThreads`
**Connection structure:**
```cpp
struct ConnectionState {
uint64_t connId;
std::string remoteAddr;
flat_str subId; // Subscription ID
std::shared_ptr<Subscription> sub;
PerMessageDeflate pmd; // Compression state
uint64_t latestEventSent = 0;
// Message parsing state
secp256k1_context *secpCtx;
std::string parseBuffer;
};
```
**Message handling pattern:**
```cpp
// WebSocket message callback
ws->onMessage([=](std::string_view msg, uWS::OpCode opCode) {
// Reuse buffer to avoid allocations
state->parseBuffer.assign(msg.data(), msg.size());
try {
auto json = nlohmann::json::parse(state->parseBuffer);
auto cmdStr = json[0].get<std::string>();
if (cmdStr == "EVENT") {
// Send to Ingester thread pool
auto packed = MsgIngester::Message(connId, std::move(json));
tpIngester->dispatchToThread(connId, std::move(packed));
}
else if (cmdStr == "REQ") {
// Send to ReqWorker thread pool
auto packed = MsgReq::Message(connId, std::move(json));
tpReqWorker->dispatchToThread(connId, std::move(packed));
}
} catch (std::exception &e) {
sendNotice("Error: " + std::string(e.what()));
}
});
```
**Critical performance optimizations:**
1. **Event batching** - Serialize event JSON once, reuse for thousands of subscribers:
```cpp
// Single serialization
std::string eventJson = event.toJson();
// Broadcast to all matching subscriptions
for (auto &[connId, sub] : activeSubscriptions) {
if (sub->matches(event)) {
sendToConnection(connId, eventJson); // Reuse serialized JSON
}
}
```
2. **Move semantics** - Zero-copy message passing:
```cpp
tpIngester->dispatchToThread(connId, std::move(message));
```
3. **Pre-allocated buffers** - Single reusable buffer per connection:
```cpp
state->parseBuffer.assign(msg.data(), msg.size());
```
4. **std::variant dispatch** - Type-safe without virtual function overhead:
```cpp
std::variant<MsgReq, MsgIngester, MsgWriter> message;
std::visit([](auto&& msg) { msg.handle(); }, message);
```
For detailed strfry implementation examples, see [references/strfry_implementation.md](references/strfry_implementation.md).
### Rust Implementation (nostr-rs-relay-style)
**Recommended libraries:**
- `tokio-tungstenite 0.17` - Async WebSocket support
- `tokio 1.x` - Async runtime
- `serde_json` - Message parsing
**WebSocket configuration:**
```rust
let config = WebSocketConfig {
max_send_queue: Some(1024),
max_message_size: settings.limits.max_ws_message_bytes,
max_frame_size: settings.limits.max_ws_frame_bytes,
..Default::default()
};
let ws_stream = WebSocketStream::from_raw_socket(
upgraded,
Role::Server,
Some(config),
).await;
```
**Connection state:**
```rust
pub struct ClientConn {
client_ip_addr: String,
client_id: Uuid,
subscriptions: HashMap<String, Subscription>,
max_subs: usize,
auth: Nip42AuthState,
}
pub enum Nip42AuthState {
NoAuth,
Challenge(String),
AuthPubkey(String),
}
```
**Async message loop with tokio::select!:**
```rust
async fn nostr_server(
repo: Arc<dyn NostrRepo>,
mut ws_stream: WebSocketStream<Upgraded>,
broadcast: Sender<Event>,
mut shutdown: Receiver<()>,
) {
let mut conn = ClientConn::new(client_ip);
let mut bcast_rx = broadcast.subscribe();
let mut ping_interval = tokio::time::interval(Duration::from_secs(300));
loop {
tokio::select! {
// Handle shutdown
_ = shutdown.recv() => { break; }
// Send periodic pings
_ = ping_interval.tick() => {
ws_stream.send(Message::Ping(Vec::new())).await.ok();
}
// Handle broadcast events (real-time)
Ok(event) = bcast_rx.recv() => {
for (id, sub) in conn.subscriptions() {
if sub.interested_in_event(&event) {
let msg = format!("[\"EVENT\",\"{}\",{}]", id,
serde_json::to_string(&event)?);
ws_stream.send(Message::Text(msg)).await.ok();
}
}
}
// Handle incoming client messages
Some(result) = ws_stream.next() => {
match result {
Ok(Message::Text(msg)) => {
handle_nostr_message(&msg, &mut conn).await;
}
Ok(Message::Binary(_)) => {
send_notice("binary messages not accepted").await;
}
Ok(Message::Ping(_) | Message::Pong(_)) => {
continue; // Auto-handled by tungstenite
}
Ok(Message::Close(_)) | Err(_) => {
break;
}
_ => {}
}
}
}
}
}
```
**Subscription filtering:**
```rust
pub struct ReqFilter {
pub ids: Option<Vec<String>>,
pub kinds: Option<Vec<u64>>,
pub since: Option<u64>,
pub until: Option<u64>,
pub authors: Option<Vec<String>>,
pub limit: Option<u64>,
pub tags: Option<HashMap<char, HashSet<String>>>,
}
impl ReqFilter {
pub fn interested_in_event(&self, event: &Event) -> bool {
self.ids_match(event)
&& self.since.map_or(true, |t| event.created_at >= t)
&& self.until.map_or(true, |t| event.created_at <= t)
&& self.kind_match(event.kind)
&& self.authors_match(event)
&& self.tag_match(event)
}
fn ids_match(&self, event: &Event) -> bool {
self.ids.as_ref()
.map_or(true, |ids| ids.iter().any(|id| event.id.starts_with(id)))
}
}
```
**Error handling:**
```rust
match ws_stream.next().await {
Some(Ok(Message::Text(msg))) => { /* handle */ }
Some(Err(WsError::Capacity(MessageTooLong{size, max_size}))) => {
send_notice(&format!("message too large ({} > {})", size, max_size)).await;
continue;
}
None | Some(Ok(Message::Close(_))) => {
info!("client closed connection");
break;
}
Some(Err(WsError::Io(e))) => {
warn!("IO error: {:?}", e);
break;
}
_ => { break; }
}
```
For detailed Rust implementation examples, see [references/rust_implementation.md](references/rust_implementation.md).
## Common Implementation Patterns
### Pattern 1: Dual Goroutine/Task Architecture
**Purpose:** Separate read and write concerns, enable ping/pong management
**Structure:**
- **Reader goroutine/task:** Blocks on `ReadMessage()`, handles incoming frames
- **Writer goroutine/task:** Sends periodic pings, processes outgoing message queue
**Benefits:**
- Natural separation of concerns
- Ping timer doesn't block message processing
- Clean shutdown coordination via context/channels
### Pattern 2: Subscription Lifecycle
**Create subscription (REQ):**
1. Parse filter from client message
2. Query database for matching stored events
3. Send stored events to client
4. Send EOSE (End of Stored Events)
5. Add subscription to active listeners for real-time events
**Handle real-time event:**
1. Check all active subscriptions
2. For each matching subscription:
- Apply filter matching logic
- Send EVENT message to client
3. Track broadcast count for monitoring
**Close subscription (CLOSE):**
1. Find subscription by ID
2. Cancel subscription context
3. Remove from active listeners
4. Clean up resources
### Pattern 3: Write Serialization
**Problem:** Concurrent writes cause panics/errors in WebSocket libraries
**Solutions:**
**Mutex approach (Go, C++):**
```go
func (ws *WebSocket) WriteJSON(v any) error {
ws.mutex.Lock()
defer ws.mutex.Unlock()
return ws.conn.WriteJSON(v)
}
```
**Single-writer goroutine (Alternative):**
```go
type writeMsg struct {
data []byte
done chan error
}
go func() {
for msg := range writeChan {
msg.done <- ws.conn.WriteMessage(websocket.TextMessage, msg.data)
}
}()
```
### Pattern 4: Connection Cleanup
**Essential cleanup steps:**
1. Cancel all subscription contexts
2. Stop ping ticker/interval
3. Remove connection from active clients map
4. Close WebSocket connection
5. Close TCP connection
6. Log connection statistics
**Go cleanup function:**
```go
kill := func() {
// Cancel contexts
cancel()
ws.cancel()
// Stop timers
ticker.Stop()
// Remove from tracking
rl.removeClientAndListeners(ws)
// Close connection
ws.conn.Close()
// Trigger hooks
for _, ondisconnect := range rl.OnDisconnect {
ondisconnect(ctx)
}
}
defer kill()
```
### Pattern 5: Event Broadcasting Optimization
**Naive approach (inefficient):**
```go
// DON'T: Serialize for each subscriber
for _, listener := range listeners {
if listener.filter.Matches(event) {
json := serializeEvent(event) // Repeated work!
listener.ws.WriteJSON(json)
}
}
```
**Optimized approach:**
```go
// DO: Serialize once, reuse for all subscribers
eventJSON, err := json.Marshal(event)
if err != nil {
return
}
for _, listener := range listeners {
if listener.filter.Matches(event) {
listener.ws.WriteMessage(websocket.TextMessage, eventJSON)
}
}
```
**Savings:** For 1000 subscribers, reduces 1000 JSON serializations to 1.
## Security Considerations
### Origin Validation
Always validate the `Origin` header for browser-based clients:
```go
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return isAllowedOrigin(origin) // Implement allowlist
},
}
```
**Default behavior:** Most libraries reject all cross-origin connections. Override with caution.
### Rate Limiting
Implement rate limits for:
- Connection establishment (per IP)
- Message throughput (per connection)
- Subscription creation (per connection)
- Event publication (per connection, per pubkey)
```go
// Example: Connection rate limiting
type rateLimiter struct {
connections map[string]*rate.Limiter
mu sync.Mutex
}
func (rl *Relay) checkRateLimit(ip string) bool {
limiter := rl.rateLimiter.getLimiter(ip)
return limiter.Allow()
}
```
### Message Size Limits
Configure limits to prevent memory exhaustion:
```go
ws.conn.SetReadLimit(maxMessageSize) // e.g., 512 KB
```
```rust
max_message_size: Some(512_000),
max_frame_size: Some(16_384),
```
### Subscription Limits
Prevent resource exhaustion:
- Max subscriptions per connection (typically 10-20)
- Max subscription ID length (prevent hash collision attacks)
- Require specific filters (prevent full database scans)
```rust
const MAX_SUBSCRIPTION_ID_LEN: usize = 256;
const MAX_SUBS_PER_CLIENT: usize = 20;
if subscriptions.len() >= MAX_SUBS_PER_CLIENT {
return Err(Error::SubMaxExceededError);
}
```
### Authentication (NIP-42)
Implement challenge-response authentication:
1. **Generate challenge on connect:**
```go
challenge := make([]byte, 8)
rand.Read(challenge)
ws.Challenge = hex.EncodeToString(challenge)
```
2. **Send AUTH challenge when required:**
```json
["AUTH", "<challenge>"]
```
3. **Validate AUTH event:**
```go
func validateAuthEvent(event *Event, challenge, relayURL string) bool {
// Check kind 22242
if event.Kind != 22242 { return false }
// Check challenge in tags
if !hasTag(event, "challenge", challenge) { return false }
// Check relay URL
if !hasTag(event, "relay", relayURL) { return false }
// Check timestamp (within 10 minutes)
if abs(time.Now().Unix() - event.CreatedAt) > 600 { return false }
// Verify signature
return event.CheckSignature()
}
```
## Performance Optimization Techniques
### 1. Connection Pooling
Reuse connections for database queries:
```go
db, _ := sql.Open("postgres", dsn)
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
```
### 2. Event Caching
Cache frequently accessed events:
```go
type EventCache struct {
cache *lru.Cache
mu sync.RWMutex
}
func (ec *EventCache) Get(id string) (*Event, bool) {
ec.mu.RLock()
defer ec.mu.RUnlock()
if val, ok := ec.cache.Get(id); ok {
return val.(*Event), true
}
return nil, false
}
```
### 3. Batch Database Queries
Execute queries concurrently for multi-filter subscriptions:
```go
var wg sync.WaitGroup
for _, filter := range filters {
wg.Add(1)
go func(f Filter) {
defer wg.Done()
events := queryDatabase(f)
sendEvents(events)
}(filter)
}
wg.Wait()
sendEOSE()
```
### 4. Compression (permessage-deflate)
Enable WebSocket compression for text frames:
```go
upgrader := websocket.Upgrader{
EnableCompression: true,
}
```
**Typical savings:** 60-80% bandwidth reduction for JSON messages
**Trade-off:** Increased CPU usage (usually worthwhile)
### 5. Monitoring and Metrics
Track key performance indicators:
- Connections (active, total, per IP)
- Messages (received, sent, per type)
- Events (stored, broadcast, per second)
- Subscriptions (active, per connection)
- Query latency (p50, p95, p99)
- Database pool utilization
```go
// Prometheus-style metrics
type Metrics struct {
Connections prometheus.Gauge
MessagesRecv prometheus.Counter
MessagesSent prometheus.Counter
EventsStored prometheus.Counter
QueryDuration prometheus.Histogram
}
```
## Testing WebSocket Implementations
### Unit Testing
Test individual components in isolation:
```go
func TestFilterMatching(t *testing.T) {
filter := Filter{
Kinds: []int{1, 3},
Authors: []string{"abc123"},
}
event := &Event{
Kind: 1,
PubKey: "abc123",
}
if !filter.Matches(event) {
t.Error("Expected filter to match event")
}
}
```
### Integration Testing
Test WebSocket connection handling:
```go
func TestWebSocketConnection(t *testing.T) {
// Start test server
server := startTestRelay(t)
defer server.Close()
// Connect client
ws, _, err := websocket.DefaultDialer.Dial(server.URL, nil)
if err != nil {
t.Fatalf("Failed to connect: %v", err)
}
defer ws.Close()
// Send REQ
req := `["REQ","test",{"kinds":[1]}]`
if err := ws.WriteMessage(websocket.TextMessage, []byte(req)); err != nil {
t.Fatalf("Failed to send REQ: %v", err)
}
// Read EOSE
_, msg, err := ws.ReadMessage()
if err != nil {
t.Fatalf("Failed to read message: %v", err)
}
if !strings.Contains(string(msg), "EOSE") {
t.Errorf("Expected EOSE, got: %s", msg)
}
}
```
### Load Testing
Use tools like `websocat` or custom scripts:
```bash
# Connect 1000 concurrent clients
for i in {1..1000}; do
(websocat "ws://localhost:8080" <<< '["REQ","test",{"kinds":[1]}]' &)
done
```
Monitor server metrics during load testing:
- CPU usage
- Memory consumption
- Connection count
- Message throughput
- Database query rate
## Debugging and Troubleshooting
### Common Issues
**1. Concurrent write panic/error**
**Symptom:** `concurrent write to websocket connection` error
**Solution:** Ensure all writes protected by mutex or use single-writer pattern
**2. Connection timeouts**
**Symptom:** Connections close after 60 seconds
**Solution:** Implement ping/pong mechanism properly:
```go
ws.SetPongHandler(func(string) error {
ws.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
```
**3. Memory leaks**
**Symptom:** Memory usage grows over time
**Common causes:**
- Subscriptions not removed on disconnect
- Event channels not closed
- Goroutines not terminated
**Solution:** Ensure cleanup function called on disconnect
**4. Slow subscription queries**
**Symptom:** EOSE delayed by seconds
**Solution:**
- Add database indexes on filtered columns
- Implement query timeouts
- Consider caching frequently accessed events
### Logging Best Practices
Log critical events with context:
```go
log.Printf(
"connection closed: cid=%s ip=%s duration=%v sent=%d recv=%d",
conn.ID,
conn.IP,
time.Since(conn.ConnectedAt),
conn.EventsSent,
conn.EventsRecv,
)
```
Use log levels appropriately:
- **DEBUG:** Message parsing, filter matching
- **INFO:** Connection lifecycle, subscription changes
- **WARN:** Rate limit violations, invalid messages
- **ERROR:** Database errors, unexpected panics
## Resources
This skill includes comprehensive reference documentation with production code examples:
### references/
- **websocket_protocol.md** - Complete RFC 6455 specification details including frame structure, opcodes, masking algorithm, and security considerations
- **khatru_implementation.md** - Go WebSocket patterns from khatru including connection lifecycle, subscription management, and performance optimizations (3000+ lines)
- **strfry_implementation.md** - C++ high-performance patterns from strfry including thread pool architecture, message batching, and zero-copy techniques (2000+ lines)
- **rust_implementation.md** - Rust async patterns from nostr-rs-relay including tokio::select! usage, error handling, and subscription filtering (2000+ lines)
Load these references when implementing specific language solutions or troubleshooting complex WebSocket issues.