Class: Tidewave::BrowserControl

Inherits:
Object
  • Object
show all
Defined in:
lib/tidewave/browser_control.rb

Overview

Server side of Tidewave browser control.

The WebSocket endpoint is a dedicated Action Cable server, since the user app may not have one, and if it does it likely has auth. Commands/replies are routed between MCP request threads and browser connections over Action Cable pub/sub streams:

* tidewave:clients      - all registered pages (used for discovery)
* tidewave:client:name  - the page registered under name
* tidewave:reply:ref    - replies to a single run_tool command

Consequently the routing works across processes whenever the configured cable adapter does, such as "solid_cable", whereas the default "async" adapter is single-process.

The pub/sub bus cannot tell whether a stream has any subscribers, so the channel broadcasts an "ack" on the reply stream as soon as it picks up a command, letting the caller fail fast when no client is connected. Similarly, when a page disconnects, its channel broadcasts "disconnected" for every command still awaiting a reply, so the caller does not wait out the full timeout.

Defined Under Namespace

Classes: Channel, ClientRegistry, Connection, Server

Constant Summary collapse

CLIENTS_STREAM =
"tidewave:clients"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cable:, logger: nil, ack_timeout: 1.0) ⇒ BrowserControl

Returns a new instance of BrowserControl.



46
47
48
49
# File 'lib/tidewave/browser_control.rb', line 46

def initialize(cable:, logger: nil, ack_timeout: 1.0)
  @ack_timeout = ack_timeout
  @server = Server.new(cable: cable, logger: logger || ::Logger.new(IO::NULL))
end

Instance Attribute Details

#serverObject (readonly)

Returns the value of attribute server.



44
45
46
# File 'lib/tidewave/browser_control.rb', line 44

def server
  @server
end

Class Method Details

.client_stream(name) ⇒ Object



36
37
38
# File 'lib/tidewave/browser_control.rb', line 36

def self.client_stream(name)
  "tidewave:client:#{name}"
end

.reply_stream(ref) ⇒ Object



40
41
42
# File 'lib/tidewave/browser_control.rb', line 40

def self.reply_stream(ref)
  "tidewave:reply:#{ref}"
end

Instance Method Details

#broadcast_run(tool_name, input, timeout_ms) ⇒ Object

Sends the tool to every connected client and returns the first reply.

Used for the discovery handshake (a browser_eval call with no sid). Returns [ :ok, reply ] or [ :error, :timeout ] when no client answered in time (the bus cannot tell whether anyone is connected).



73
74
75
# File 'lib/tidewave/browser_control.rb', line 73

def broadcast_run(tool_name, input, timeout_ms)
  call_tool(CLIENTS_STREAM, tool_name, nil, input, timeout_ms, await_ack: false)
end

#call(env) ⇒ Object

Rack entrypoint for the WebSocket endpoint.



52
53
54
# File 'lib/tidewave/browser_control.rb', line 52

def call(env)
  @server.call(env)
end

#run(sid, tool_name, input, timeout_ms) ⇒ Object

Runs the tool against the client owning sid and waits for the reply. timeout_ms may be nil to wait indefinitely.

Returns [ :ok, reply ] (the page's response) or [ :error, reason ], where reason is :invalid_sid, :unknown_client, :timeout, or :disconnected.



61
62
63
64
65
66
# File 'lib/tidewave/browser_control.rb', line 61

def run(sid, tool_name, input, timeout_ms)
  name = parse_sid(sid)
  return [ :error, :invalid_sid ] unless name

  call_tool(self.class.client_stream(name), tool_name, sid, input, timeout_ms, await_ack: true)
end