ZenWebsocket Usage Rules

View Source

Core Principles

  1. Start Simple: Use direct connection for development, add supervision for production
  2. Only 5 Functions: The entire public API is just 5 functions
  3. Real API Testing: Always test against real endpoints, never mock WebSocket behavior

Quick Start Pattern

# Simplest possible usage - connect and send
{:ok, client} = ZenWebsocket.Client.connect("wss://test.deribit.com/ws/api/v2")
ZenWebsocket.Client.send_message(client, Jason.encode!(%{method: "public/test"}))

The 5 Essential Functions

# 1. Connect to WebSocket
{:ok, client} = ZenWebsocket.Client.connect(url, opts)

# 2. Send messages
:ok = ZenWebsocket.Client.send_message(client, message)

# 3. Subscribe to channels
{:ok, subscription_id} = ZenWebsocket.Client.subscribe(client, channels)

# 4. Check connection state
state = ZenWebsocket.Client.get_state(client)  # :connected, :connecting, :disconnected

# 5. Close connection
:ok = ZenWebsocket.Client.close(client)

Common Patterns

Pattern 1: Development/Testing (No Supervision)

# Direct connection - crashes won't restart
{:ok, client} = ZenWebsocket.Client.connect(url)
# Use the client...
ZenWebsocket.Client.close(client)

Pattern 2: Production with Dynamic Connections

# Add to your supervision tree
children = [
  ZenWebsocket.ClientSupervisor,
  # ... other children
]

# Start connections dynamically
{:ok, client} = ZenWebsocket.ClientSupervisor.start_client(url, opts)

Pattern 3: Production with Fixed Connections

# Add specific clients to supervision tree
children = [
  {ZenWebsocket.Client, [
    url: "wss://api.example.com/ws",
    id: :main_websocket,
    heartbeat_config: %{type: :ping, interval: 30_000}
  ]}
]

Configuration Options

opts = [
  # Connection
  timeout: 5000,              # Connection timeout in ms
  headers: [],                # Additional headers
  
  # Reconnection
  retry_count: 3,             # Max reconnection attempts
  retry_delay: 1000,          # Initial retry delay (exponential backoff)
  reconnect_on_error: true,   # Auto-reconnect on errors
  
  # Heartbeat
  heartbeat_config: %{
    type: :ping,              # :ping, :pong, :deribit, :custom
    interval: 30_000,         # Heartbeat interval in ms
    message: nil              # Custom heartbeat message (for :custom type)
  }
]

Platform-Specific Rules

Deribit Integration

# Use the Deribit adapter for complete integration
{:ok, adapter} = ZenWebsocket.Examples.DeribitAdapter.start_link([
  url: "wss://test.deribit.com/ws/api/v2",
  client_id: System.get_env("DERIBIT_CLIENT_ID"),
  client_secret: System.get_env("DERIBIT_CLIENT_SECRET")
])

# The adapter handles:
# - Authentication flow
# - Heartbeat/test_request
# - Subscription management
# - Cancel-on-disconnect

Error Handling

# All functions return tagged tuples
case ZenWebsocket.Client.connect(url) do
  {:ok, client} -> 
    # Success path
    client
    
  {:error, reason} ->
    # Errors are passed raw from Gun/WebSocket
    # Common errors: :timeout, :connection_refused, :protocol_error
    Logger.error("Connection failed: #{inspect(reason)}")
end

Testing Rules

# NEVER mock WebSocket connections
# Use real test endpoints
@tag :integration
test "real WebSocket behavior" do
  {:ok, client} = ZenWebsocket.Client.connect("wss://test.deribit.com/ws/api/v2")
  # Test against real API...
end

# For controlled testing, use MockWebSockServer
{:ok, server} = ZenWebsocket.MockWebSockServer.start(port: 8080)
{:ok, client} = ZenWebsocket.Client.connect("ws://localhost:8080")

DO NOT

  1. Don't create wrapper modules - Use the 5 functions directly
  2. Don't mock WebSocket behavior - Test against real endpoints
  3. Don't add custom reconnection - Use built-in retry options
  4. Don't transform errors - Handle raw Gun/WebSocket errors
  5. Don't avoid GenServers - Client uses GenServer appropriately for state

Architecture Notes

  • Gun Transport: Built on Gun for HTTP/2 and WebSocket
  • GenServer State: Client maintains connection state in GenServer
  • ETS Registry: Fast connection lookups via ETS
  • Exponential Backoff: Smart reconnection with backoff
  • Real API Testing: 93 tests, all using real APIs

Monitoring

# Telemetry events are emitted for monitoring
:telemetry.attach(
  "websocket-logger",
  [:zen_websocket, :client, :message_received],
  fn _event, measurements, metadata, _config ->
    Logger.info("Message received: #{measurements.size} bytes")
  end,
  nil
)

Module Limits

Each module follows strict simplicity rules:

  • Maximum 5 public functions per module
  • Maximum 15 lines per function
  • Maximum 2 levels of function calls
  • Real API testing only (no mocks)

Getting Help

  • Examples: See lib/zen_websocket/examples/ directory
  • Tests: Review test/ for usage patterns
  • Deribit: See DeribitAdapter for complete platform integration

Common Mistakes to Avoid

  1. Creating abstractions too early - Start with direct usage
  2. Mocking in tests - Always use real WebSocket endpoints
  3. Custom error types - Handle raw Gun/WebSocket errors
  4. Complex supervision - Use provided patterns (1, 2, or 3)
  5. Ignoring heartbeats - Configure heartbeat for production

Migration from Other Libraries

From Websockex

# Old (Websockex with callbacks)
defmodule MyClient do
  use WebSockex
  def handle_frame({:text, msg}, state), do: {:ok, state}
end

# New (ZenWebsocket - simpler)
{:ok, client} = ZenWebsocket.Client.connect(url)
# Messages handled via message_handler configuration

From Gun directly

# You're already using the right approach!
# ZenWebsocket is a thin, focused layer over Gun

Performance Characteristics

  • Connection Time: < 100ms typical
  • Message Latency: < 1ms processing
  • Memory: ~50KB per connection
  • Reconnection: Exponential backoff (1s, 2s, 4s...)
  • Concurrency: Thousands of simultaneous connections

Required Environment Variables

For platform integrations:

# Deribit
export DERIBIT_CLIENT_ID="your_client_id"
export DERIBIT_CLIENT_SECRET="your_client_secret"

Best Practices Summary

  1. Start with Pattern 1 (direct) for development
  2. Move to Pattern 2 or 3 for production
  3. Configure heartbeats for long-lived connections
  4. Test against real endpoints
  5. Handle raw errors with pattern matching
  6. Use telemetry for monitoring
  7. Keep it simple - just 5 functions!