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.



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 84

def run!
  install_traps if @signal_traps
  backoff = 1

  Async do |top|
    @top_task = top
    signal_watcher = start_signal_watcher(top)
    until @stop
      begin
        run_one_connection(top)
        backoff = 1
      rescue Interrupt, Async::Stop
        # Ctrl-C / SIGTERM / task.stop: exit cleanly.
        @stop = true
        break
      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
  rescue Interrupt
    # Signal arrived while not inside a connection; just exit.
    @stop = true
  ensure
    signal_watcher&.stop
    close_signal_pipe
    @top_task = nil
  end
end

#send_close(sub_id) ⇒ Object

Helper: send [“CLOSE”, sub_id]



140
141
142
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 140

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

#send_event(event_hash) ⇒ Object

Helper: send [“EVENT”, event_hash]



135
136
137
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 135

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:



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

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, …]



130
131
132
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 130

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
76
77
78
79
80
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 64

def stop!
  @stop = true
  # Poke the signal pipe if available (works from any thread); the
  # watcher task will call @top_task.stop from inside the reactor.
  # If we're already inside the reactor thread/fiber, we can stop
  # the top task directly.
  if @signal_pipe_w
    begin
      @signal_pipe_w.write_nonblock('.')
    rescue IO::WaitWritable, Errno::EPIPE, IOError
      nil
    end
  else
    task = @top_task
    task&.stop
  end
end