Class: NwcRuby::Transport::RelayConnection
- Inherits:
-
Object
- Object
- NwcRuby::Transport::RelayConnection
- 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
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#url ⇒ Object
readonly
Returns the value of attribute url.
Instance Method Summary collapse
-
#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
constructor
A new instance of RelayConnection.
- #on_error(&block) ⇒ Object
- #on_event(&block) ⇒ Object
- #on_open(&block) ⇒ Object
- #on_poll(&block) ⇒ Object
-
#run! ⇒ Object
Blocks forever, reconnecting as needed, until #stop! is called or SIGTERM / SIGINT is received.
-
#send_close(sub_id) ⇒ Object
Helper: send [“CLOSE”, sub_id].
-
#send_event(event_hash) ⇒ Object
Helper: send [“EVENT”, event_hash].
-
#send_message(message) ⇒ Object
Send raw client->relay message (e.g. REQ, EVENT, CLOSE).
-
#send_req(sub_id:, filters:) ⇒ Object
Helper: send [“REQ”, sub_id, filter1, filter2, …].
- #stop! ⇒ Object
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
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
34 35 36 |
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 34 def logger @logger end |
#url ⇒ Object (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.}") @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) (['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) (['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.
122 123 124 125 126 127 |
# File 'lib/nwc_ruby/transport/relay_connection.rb', line 122 def () raise TransportError, 'not connected' unless @conn @conn.write(Protocol::WebSocket::TextMessage.generate()) @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:) (['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 |