Class: Plushie::Runtime

Inherits:
Object
  • Object
show all
Includes:
Commands, Subscriptions
Defined in:
lib/plushie/runtime.rb,
lib/plushie/runtime/windows.rb,
lib/plushie/runtime/commands.rb,
lib/plushie/runtime/subscriptions.rb

Overview

Core event loop for Plushie applications.

Owns the Elm-style update cycle: event -> model -> view -> diff -> patch. Processes events sequentially from a thread-safe queue. All state is owned by the runtime thread: no shared mutable state.

Defined Under Namespace

Modules: Commands, Subscriptions, Windows

Constant Summary collapse

SDK_LOG_LEVELS =
{
  off: Logger::UNKNOWN,
  error: Logger::ERROR,
  warning: Logger::WARN,
  warn: Logger::WARN,
  info: Logger::INFO,
  debug: Logger::DEBUG,
  trace: Logger::DEBUG
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app:, transport: :spawn, format: :msgpack, daemon: false, binary: nil, log_level: DEFAULT_LOG_LEVEL, token: nil, dev: false, dev_dirs: nil) ⇒ Runtime

Returns a new instance of Runtime.

Parameters:

  • app (Object)

    app instance (includes Plushie::App)

  • transport (:spawn, :stdio, Array(:iostream, adapter)) (defaults to: :spawn)

    transport mode

  • format (:msgpack, :json) (defaults to: :msgpack)

    wire format

  • daemon (Boolean) (defaults to: false)

    keep running after last window closes

  • binary (String, nil) (defaults to: nil)

    renderer binary path

  • log_level (Symbol) (defaults to: DEFAULT_LOG_LEVEL)

    SDK logger level and fallback renderer log level. Omitted keeps the SDK logger at warn and the renderer fallback at error.

  • token (String, nil) (defaults to: nil)

    authentication token for the renderer

  • dev (Boolean) (defaults to: false)

    enable live code reloading via DevServer

  • dev_dirs (Array<String>, nil) (defaults to: nil)

    directories to watch (default: ["lib/"])



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/plushie/runtime.rb', line 47

def initialize(app:, transport: :spawn, format: :msgpack, daemon: false,
  binary: nil, log_level: DEFAULT_LOG_LEVEL, token: nil, dev: false, dev_dirs: nil)
  validate_app!(app)
  validate_transport!(transport)

  @app = app
  @transport = transport
  @format = format
  @daemon = daemon
  @binary = binary
  @log_level_explicit = !log_level.equal?(DEFAULT_LOG_LEVEL)
  @log_level = renderer_log_level(log_level)
  @token = token
  @dev = dev
  @dev_dirs = dev_dirs

  @event_queue = BoundedQueue.new
  @model = nil
  @previous_tree = nil
  @bridge = nil
  @dev_server = nil
  @running = false
  @timer_scheduler = TimerScheduler.new

  @async_tasks = {}        # tag -> {thread:, nonce:}
  @pending_effects = {}    # wire_id -> timer_thread
  @effect_tags = {}        # tag -> wire_id
  @effect_ids = {}         # wire_id -> tag
  @effect_kinds = {}       # wire_id -> kind string

  # Coalescable event buffer. High-frequency events (move, scroll,
  # scrolled, resize) are stored here, keyed by (window_id, id, type),
  # and flushed at the next event_queue iteration. Last-wins so
  # bursts collapse to the latest value, except scroll events which
  # accumulate their delta_x / delta_y. Keeps update() from drowning
  # in pointer-move events when the host's update() is slow.
  @pending_coalesce = {}   # [window_id, id, type] -> event
  @coalesce_order = []     # insertion order for deterministic flush
  @pending_timers = {}     # event_key -> {thread:, nonce:}
  @subscriptions = {}      # sub_key -> {sub_type:, ...}
  @subscription_keys = []  # sorted keys for short-circuit
  @canvas_widgets = {}     # scoped_id -> CanvasWidget::RegistryEntry
  @consecutive_errors = 0
  @consecutive_view_errors = 0
  @widget_statuses = {}    # id -> status string
  @focused_widget_id = nil # currently focused widget ID
  @memo_cache = {} # : Hash[untyped, untyped]
  @diagnostics = []        # accumulated prop validation diagnostics
  @diagnostics_mutex = Mutex.new
  @dispatch_depth = 0      # Command.dispatch chain position
  @pending_runtime_events = [] # : Array[untyped]
  @pending_stub_acks = {}  # kind -> Queue (for sync ack round-trip)
  @pending_await_async = {} # tag -> Queue (for sync await)
  @pending_interact = nil   # {id:, action:, selector:, result_queue:, timeout_timer:} for current interact
  @tracked_windows = Set.new # active window IDs
  @restarting = false
  @runtime_thread = nil

  @logger = Logger.new($stderr, level: sdk_log_level(log_level), progname: "plushie")
end

Instance Attribute Details

#appObject (readonly)

Accessors for Runtime submodules (Windows, etc.).



35
36
37
# File 'lib/plushie/runtime.rb', line 35

def app
  @app
end

#loggerObject (readonly)

Accessors for Runtime submodules (Windows, etc.).



35
36
37
# File 'lib/plushie/runtime.rb', line 35

def logger
  @logger
end

#modelObject (readonly)

Accessors for Runtime submodules (Windows, etc.).



35
36
37
# File 'lib/plushie/runtime.rb', line 35

def model
  @model
end

Instance Method Details

#await_async(tag, timeout: 5) ⇒ :ok

Waits for an async task with the given tag to complete.

If the task has already completed, returns immediately. Otherwise blocks until the task finishes and its result has been processed through update.

Parameters:

  • tag (Symbol)

    the async command tag

  • timeout (Numeric) (defaults to: 5)

    max wait in seconds

Returns:

  • (:ok)

Raises:



251
252
253
254
255
256
257
258
259
260
# File 'lib/plushie/runtime.rb', line 251

def await_async(tag, timeout: 5)
  ack_queue = Thread::Queue.new
  enqueued = BoundedQueue.push(@event_queue, [:await_async, tag, ack_queue], timeout: Float(timeout))
  raise Plushie::Error, format_await_async_timeout(tag) if enqueued.nil?

  result = ack_queue.pop(timeout: Float(timeout))
  raise Plushie::Error, format_await_async_timeout(tag) if result.nil?

  :ok
end

#bridge_send_window_op(op, window_id, settings = {}) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Send a window operation to the renderer via the bridge.



213
214
215
216
# File 'lib/plushie/runtime.rb', line 213

def bridge_send_window_op(op, window_id, settings = {})
  bridge = @bridge or return
  bridge.send_encoded(Protocol::Encode.encode_window_op(op, window_id, Encode.encode_props(settings), @format))
end

#get_diagnosticsArray<Event::System>

Returns and clears accumulated prop validation diagnostics.

The renderer emits diagnostic events when validate_props is enabled. These are intercepted by the runtime (never delivered to update) and accumulated. This method atomically retrieves and clears the list.

Returns:



187
188
189
190
191
192
193
# File 'lib/plushie/runtime.rb', line 187

def get_diagnostics
  @diagnostics_mutex.synchronize do
    result = @diagnostics.dup
    @diagnostics.clear
    result
  end
end

#get_focusedString?

Returns the ID of the currently focused widget, or nil. Focus is tracked automatically from renderer status events.

Returns:

  • (String, nil)


199
200
201
# File 'lib/plushie/runtime.rb', line 199

def get_focused
  @focused_widget_id
end

#interact(action, selector = nil, payload = {}, timeout: 5) ⇒ Array<Object>

Simulate a user interaction with a widget.

Sends an interact message through the bridge and blocks until the renderer responds. Used by scripting and automation: the test session has its own interact that runs synchronously within the test process.

Parameters:

  • action (String)

    interaction type ("click", "type_text", etc.)

  • selector (Hash, nil) (defaults to: nil)

    target widget selector ("id", value: "btn")

  • payload (Hash) (defaults to: {})

    action-specific parameters

  • timeout (Numeric) (defaults to: 5)

    max wait in seconds

Returns:

  • (Array<Object>)

    events produced by the interaction

Raises:



230
231
232
233
234
235
236
237
238
239
240
# File 'lib/plushie/runtime.rb', line 230

def interact(action, selector = nil, payload = {}, timeout: 5)
  result_queue = Thread::Queue.new
  enqueued = BoundedQueue.push(@event_queue, [:interact, action, selector, payload, result_queue], timeout: Float(timeout))
  raise Plushie::Error, format_interact_timeout(action, selector) if enqueued.nil?

  result = result_queue.pop(timeout: Float(timeout))
  raise Plushie::Error, format_interact_timeout(action, selector) if result.nil?
  raise Plushie::Error, result[:error] if result.is_a?(Hash) && result[:error]

  result.is_a?(Hash) ? result.fetch(:events, []) : []
end

#register_effect_stub(kind, response, timeout: 5) ⇒ Object

Register an effect stub with the renderer. Blocks until the renderer confirms the stub is stored.

Parameters:

  • kind (String)

    effect kind (e.g. "clipboard_read")

  • response (Object)

    the canned response to return

  • timeout (Numeric) (defaults to: 5)

    max wait in seconds

Raises:



153
154
155
156
157
158
159
160
161
162
# File 'lib/plushie/runtime.rb', line 153

def register_effect_stub(kind, response, timeout: 5)
  ack_queue = Thread::Queue.new
  enqueued = BoundedQueue.push(@event_queue, [:register_effect_stub, kind, response, ack_queue], timeout: Float(timeout))
  raise Plushie::Error, "effect stub registration timed out for #{kind}" if enqueued.nil?

  result = ack_queue.pop(timeout: Float(timeout))
  raise Plushie::Error, "effect stub registration timed out for #{kind}" if result.nil?

  :ok
end

#runObject

Run the event loop in the calling thread (blocking).



109
110
111
112
113
114
115
116
117
118
# File 'lib/plushie/runtime.rb', line 109

def run
  @runtime_thread = Thread.current
  start_bridge
  start_dev_server if @dev
  initialize_app
  event_loop
ensure
  shutdown
  @runtime_thread = nil
end

#startRuntime

Start the event loop in a background thread.

Returns:



122
123
124
125
126
127
# File 'lib/plushie/runtime.rb', line 122

def start
  thread = Thread.new { run }
  thread.name = "plushie-runtime"
  @loop_thread = thread
  self
end

#stopObject

Stop a background runtime.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/plushie/runtime.rb', line 130

def stop
  @running = false
  enqueued = BoundedQueue.push(@event_queue, :shutdown, timeout: 1)
  thread = @loop_thread
  return unless thread
  return if thread == Thread.current

  unless enqueued
    @event_queue.close if @event_queue.respond_to?(:close)
    stop_thread(thread, timeout: 5)
    return
  end

  joined = thread.join(5)
  stop_thread(thread, timeout: 1) if joined.nil?
end

#unregister_effect_stub(kind, timeout: 5) ⇒ Object

Remove a previously registered effect stub. Blocks until the renderer confirms the stub is removed.

Parameters:

  • kind (String)

    effect kind

  • timeout (Numeric) (defaults to: 5)

    max wait in seconds

Raises:



169
170
171
172
173
174
175
176
177
178
# File 'lib/plushie/runtime.rb', line 169

def unregister_effect_stub(kind, timeout: 5)
  ack_queue = Thread::Queue.new
  enqueued = BoundedQueue.push(@event_queue, [:unregister_effect_stub, kind, ack_queue], timeout: Float(timeout))
  raise Plushie::Error, "effect stub unregistration timed out for #{kind}" if enqueued.nil?

  result = ack_queue.pop(timeout: Float(timeout))
  raise Plushie::Error, "effect stub unregistration timed out for #{kind}" if result.nil?

  :ok
end

#view_error?Boolean

Returns true if the most recent view/render call failed, meaning the tree is stale and does not reflect the current model state.

Returns:

  • (Boolean)


207
208
209
# File 'lib/plushie/runtime.rb', line 207

def view_error?
  @consecutive_view_errors > 0
end