Class: Smplkit::SharedWebSocket
- Inherits:
-
Object
- Object
- Smplkit::SharedWebSocket
- Defined in:
- lib/smplkit/ws.rb
Overview
Manages a single WebSocket connection to the app service event gateway.
A single SharedWebSocket instance is shared across all product modules (config, flags, logging) within one Smplkit::Client. Product modules register listeners for specific event types; the shared connection dispatches incoming events to the appropriate listeners.
The connection runs on a dedicated SDK-owned thread that hosts the Async reactor and the underlying async-websocket I/O. Public methods are thread-safe and non-blocking.
Gateway protocol (mirrors the Python reference in smplkit._ws):
- Connect to +wss://app.<base_domain>/api/ws/v1/events?api_key={key}+
- Receive +{"type": "connected"}+ on success
- Receive events: +{"event": "config_changed", ...}+, etc.
- No subscribe message — the API key determines the account
- Heartbeat: server sends +"ping"+ (text), client responds with +"pong"+
On disconnect the reactor reconnects with exponential backoff (1, 2, 4, 8, 16, 32, 60 seconds, then capped). stop closes the connection from the outer thread; the reader exits and the daemon thread terminates.
Constant Summary collapse
- BACKOFF_SCHEDULE =
[1, 2, 4, 8, 16, 32, 60].freeze
- USER_AGENT =
"smplkit-ruby-sdk/#{Smplkit::VERSION}".freeze
Instance Attribute Summary collapse
-
#connection_status ⇒ Object
readonly
—– Connection status —————————————-.
Instance Method Summary collapse
-
#build_ws_url ⇒ Object
—– URL builder ———————————————-.
-
#dispatch(event_name, data) ⇒ Object
Dispatch
datato every listener registered forevent_name. -
#handle_inbound(text, send_pong:) ⇒ Object
Process a single inbound text frame the way the live reactor does: “ping” → call
send_pongwith “pong”; otherwise parse JSON and, if a “event” key is present, dispatch to listeners. -
#initialize(app_base_url:, api_key:, metrics: nil) ⇒ SharedWebSocket
constructor
A new instance of SharedWebSocket.
- #off(event_name, callback) ⇒ Object
-
#on(event_name, &callback) ⇒ Object
—– Listener registration ————————————.
-
#start ⇒ Object
—– Lifecycle ————————————————.
- #stop ⇒ Object
Constructor Details
#initialize(app_base_url:, api_key:, metrics: nil) ⇒ SharedWebSocket
Returns a new instance of SharedWebSocket.
39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/smplkit/ws.rb', line 39 def initialize(app_base_url:, api_key:, metrics: nil) @app_base_url = app_base_url @api_key = api_key @metrics = metrics @listeners = Hash.new { |h, k| h[k] = [] } @listeners_lock = Mutex.new @connection_status = "disconnected" @closed = false @ws_thread = nil @connection = nil @connection_lock = Mutex.new end |
Instance Attribute Details
#connection_status ⇒ Object (readonly)
—– Connection status —————————————-
76 77 78 |
# File 'lib/smplkit/ws.rb', line 76 def connection_status @connection_status end |
Instance Method Details
#build_ws_url ⇒ Object
—– URL builder ———————————————-
105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/smplkit/ws.rb', line 105 def build_ws_url url = @app_base_url.dup ws_url = if url.start_with?("https://") "wss://#{url[("https://".length)..]}" elsif url.start_with?("http://") "ws://#{url[("http://".length)..]}" else "wss://#{url}" end ws_url = ws_url.chomp("/") "#{ws_url}/api/ws/v1/events?api_key=#{@api_key}" end |
#dispatch(event_name, data) ⇒ Object
Dispatch data to every listener registered for event_name. Listener exceptions are caught and logged; one bad listener never blocks the rest.
65 66 67 68 69 70 71 72 |
# File 'lib/smplkit/ws.rb', line 65 def dispatch(event_name, data) callbacks = @listeners_lock.synchronize { @listeners[event_name].dup } callbacks.each do |cb| cb.call(data) rescue StandardError => e Smplkit.debug("websocket", "listener for #{event_name} raised: #{e.class}: #{e.}") end end |
#handle_inbound(text, send_pong:) ⇒ Object
Process a single inbound text frame the way the live reactor does: “ping” → call send_pong with “pong”; otherwise parse JSON and, if a “event” key is present, dispatch to listeners.
Returns one of :ping, :event, :no_event, :unparseable for the caller to log/observe; the live reactor ignores the return value.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/smplkit/ws.rb', line 127 def handle_inbound(text, send_pong:) if text == "ping" send_pong.call("pong") return :ping end data = begin JSON.parse(text) rescue JSON::ParserError return :unparseable end event = data["event"] if event dispatch(event, data) :event else :no_event end end |
#off(event_name, callback) ⇒ Object
58 59 60 |
# File 'lib/smplkit/ws.rb', line 58 def off(event_name, callback) @listeners_lock.synchronize { @listeners[event_name].delete(callback) } end |
#on(event_name, &callback) ⇒ Object
—– Listener registration ————————————
54 55 56 |
# File 'lib/smplkit/ws.rb', line 54 def on(event_name, &callback) @listeners_lock.synchronize { @listeners[event_name] << callback } end |
#start ⇒ Object
—– Lifecycle ————————————————
80 81 82 83 84 85 86 87 88 |
# File 'lib/smplkit/ws.rb', line 80 def start return if @ws_thread&.alive? Smplkit.debug("websocket", "starting shared WebSocket background thread") @closed = false @connection_status = "connecting" @ws_thread = Thread.new { run_reactor } @ws_thread.name = "smplkit-shared-ws" if @ws_thread.respond_to?(:name=) end |
#stop ⇒ Object
90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/smplkit/ws.rb', line 90 def stop Smplkit.debug("websocket", "stopping shared WebSocket") @closed = true @connection_status = "disconnected" close_active_connection thread = @ws_thread @ws_thread = nil return unless thread thread.join(2.0) thread.kill if thread.alive? end |