Class: NwcRuby::Transport::RelayConnection

Inherits:
Object
  • Object
show all
Defined in:
lib/nwc_ruby/transport/relay_connection.rb

Overview

A reliable long-running connection to a Nostr relay.

This is the reliability layer: everything the developer shouldn’t have to think about. It handles:

- RFC 6455 ping every `ping_interval` seconds (keeps middleboxes from
  idle-closing the socket; the relay's pong reply is handled by the
  protocol layer automatically)
- forced recycle every `recycle_interval` (belt-and-suspenders against
  relay bugs or silent connection death)
- capped exponential backoff on reconnect (1s → 2 → 4 → ... → 60s)
- SIGTERM / SIGINT handling for clean Kamal deploys

Usage:

conn = RelayConnection.new(url: "wss://relay.rizful.com")
conn.on_event { |event_hash| ... }
conn.on_open  { |c| c.send_req(sub_id: "foo", filters: [...]) }
conn.run!  # blocks until stop! or signal

Constant Summary collapse

DEFAULT_PING_INTERVAL =
15
DEFAULT_RECYCLE_INTERVAL =
300
DEFAULT_MAX_BACKOFF =
60

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url:, ping_interval: DEFAULT_PING_INTERVAL, recycle_interval: DEFAULT_RECYCLE_INTERVAL, max_backoff: DEFAULT_MAX_BACKOFF, poll_interval: nil, logger: default_logger, install_signal_traps: true) ⇒ RelayConnection

Returns a new instance of RelayConnection.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 36

def initialize(url:,
               ping_interval: DEFAULT_PING_INTERVAL,
               recycle_interval: DEFAULT_RECYCLE_INTERVAL,
               max_backoff: DEFAULT_MAX_BACKOFF,
               poll_interval: nil,
               logger: default_logger,
               install_signal_traps: true)
  @url              = url
  @ping_interval    = ping_interval
  @recycle_interval = recycle_interval
  @max_backoff      = max_backoff
  @poll_interval    = poll_interval
  @logger           = logger

  @event_cb         = nil
  @open_cb          = nil
  @error_cb         = nil
  @poll_cb          = nil
  @stop             = false
  @signal_traps     = install_signal_traps
  @top_task         = nil
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.



34
35
36
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 34

def logger
  @logger
end

#urlObject (readonly)

Returns the value of attribute url.



34
35
36
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 34

def url
  @url
end

Instance Method Details

#on_error(&block) ⇒ Object



61
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 61

def on_error(&block) = @error_cb = block

#on_event(&block) ⇒ Object



59
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 59

def on_event(&block) = @event_cb = block

#on_open(&block) ⇒ Object



60
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 60

def on_open(&block) = @open_cb = block

#on_poll(&block) ⇒ Object



62
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 62

def on_poll(&block) = @poll_cb = block

#run!Object

Blocks forever, reconnecting as needed, until #stop! is called or SIGTERM / SIGINT is received.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 79

def run!
  install_traps if @signal_traps
  backoff = 1

  Async do |top|
    @top_task = top
    until @stop
      begin
        run_one_connection(top)
        backoff = 1
      rescue StandardError => e
        break if @stop

        @logger.warn("[nwc] connection failed: #{e.class}: #{e.message}")
        @error_cb&.call(e)
        sleep_seconds = [backoff, @max_backoff].min
        @logger.info("[nwc] reconnecting in #{sleep_seconds}s")
        sleep sleep_seconds
        backoff *= 2
      end
    end
  ensure
    @top_task = nil
  end
end

#send_close(sub_id) ⇒ Object

Helper: send [“CLOSE”, sub_id]



125
126
127
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 125

def send_close(sub_id)
  send_message(['CLOSE', sub_id])
end

#send_event(event_hash) ⇒ Object

Helper: send [“EVENT”, event_hash]



120
121
122
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 120

def send_event(event_hash)
  send_message(['EVENT', event_hash])
end

#send_message(message) ⇒ Object

Send raw client->relay message (e.g. REQ, EVENT, CLOSE). Safe to call from within on_open / on_event callbacks.

Raises:



107
108
109
110
111
112
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 107

def send_message(message)
  raise TransportError, 'not connected' unless @conn

  @conn.write(Protocol::WebSocket::TextMessage.generate(message))
  @conn.flush
end

#send_req(sub_id:, filters:) ⇒ Object

Helper: send [“REQ”, sub_id, filter1, filter2, …]



115
116
117
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 115

def send_req(sub_id:, filters:)
  send_message(['REQ', sub_id, *Array(filters)])
end

#stop!Object



64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 64

def stop!
  @stop = true
  # Close the websocket to unblock the read loop. The @stop flag
  # will cause the run loop to exit on its own. We avoid calling
  # @top_task.stop here because stop! may be called from a
  # different thread than the Async reactor.
  begin
    @conn&.close
  rescue StandardError
    nil
  end
end