Class: Smplkit::SharedWebSocket

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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_statusObject (readonly)

—– Connection status —————————————-



76
77
78
# File 'lib/smplkit/ws.rb', line 76

def connection_status
  @connection_status
end

Instance Method Details

#build_ws_urlObject

—– 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.message}")
  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

#startObject

—– 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

#stopObject



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