Module: Plushie::Runtime::Commands
- Included in:
- Plushie::Runtime
- Defined in:
- lib/plushie/runtime/commands.rb,
sig/plushie/runtime.rbs
Overview
Command execution engine for the Plushie runtime.
Handles all Command::Cmd types returned by app.update and app.init. Included into Runtime as a mixin.
Constant Summary collapse
- DISPATCH_DEPTH_LIMIT =
Maximum synchronous
Command.dispatchchain depth before the runtime guard fires.Command.dispatchqueues a follow-up event back through the runtime mailbox; a pathologicalupdatethat keeps returning another dispatch would fill the queue indefinitely, so the runtime caps the chain and surfaces a typedDispatchLoopExceededdiagnostic. 100
Instance Method Summary collapse
-
#cancel_task(tag)
Cancel a running async/stream task.
- #enqueue_runtime_event(message) ⇒ Object
-
#execute_async(callable, tag)
Spawn a dedicated thread for async work with nonce tracking.
-
#execute_commands(cmd)
Execute a command or list of commands, threading state.
-
#execute_done(value, mapper)
Dispatch a done command immediately.
-
#execute_effect(payload)
Execute an effect request (send to renderer + start timeout).
-
#execute_send_after(delay_ms, event)
Schedule a delayed event.
-
#execute_stream(callable, tag)
Spawn a thread for streaming work with emit callback.
-
#send_advance_frame(timestamp)
Advance the animation clock.
-
#send_command(payload)
Send a single widget-targeted command via the unified wire format.
-
#send_commands(commands)
Send batched widget-targeted commands.
-
#send_image_op(payload)
Send an image operation.
-
#send_load_font(family, data)
Send a typed load_font message to the renderer.
-
#send_system_op(payload)
Send a system-level operation.
-
#send_system_query(payload)
Send a system-level query.
-
#send_widget_op(op, payload)
Send a widget operation to the renderer.
-
#send_window_op(payload)
Send a window operation to the renderer.
-
#send_window_query(payload)
Send a window query (response arrives as effect_response or op_query_response).
- #stop_thread(thread, timeout: 0.5)
Instance Method Details
#cancel_task(tag)
This method returns an undefined value.
Cancel a running async/stream task. Marks the entry as cancelled instead of deleting it. The async result handler owns cleanup, preventing a race where Thread.kill triggers the rescue block that pushes an async_result after the entry was already deleted.
131 132 133 134 135 136 137 |
# File 'lib/plushie/runtime/commands.rb', line 131 def cancel_task(tag) entry = @async_tasks[tag] return unless entry && entry[:nonce] != :cancelled stop_thread(entry[:thread]) @async_tasks[tag] = {nonce: :cancelled} end |
#enqueue_runtime_event(message) ⇒ Object
173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/plushie/runtime/commands.rb', line 173 def enqueue_runtime_event() unless Thread.current == @runtime_thread return BoundedQueue.push(@event_queue, ) end queued = BoundedQueue.try_push(@event_queue, ) return queued if queued @pending_runtime_events << { message: , remaining: @event_queue.length } end |
#execute_async(callable, tag)
This method returns an undefined value.
Spawn a dedicated thread for async work with nonce tracking.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/plushie/runtime/commands.rb', line 84 def execute_async(callable, tag) cancel_task(tag) nonce = rand(1 << 64) queue = @event_queue thread = Thread.new do result = callable.call BoundedQueue.push(queue, [:async_result, tag, nonce, result]) rescue => e BoundedQueue.push(queue, [:async_result, tag, nonce, [:error, e]]) end thread.name = "plushie-async-#{tag}" @async_tasks[tag] = {thread: thread, nonce: nonce} end |
#execute_commands(cmd)
This method returns an undefined value.
Execute a command or list of commands, threading state.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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 |
# File 'lib/plushie/runtime/commands.rb', line 24 def execute_commands(cmd) return if cmd.nil? case cmd.type when :none then nil when :batch then cmd.payload[:commands]&.each { |c| execute_commands(c) } when :task then execute_async(cmd.payload[:callable], cmd.payload[:tag]) when :stream then execute_stream(cmd.payload[:callable], cmd.payload[:tag]) when :cancel then cancel_task(cmd.payload[:tag]) when :dispatch then execute_done(cmd.payload[:value], cmd.payload[:mapper]) when :send_after then execute_send_after(cmd.payload[:delay], cmd.payload[:event]) when :exit then @running = false # Unified widget-targeted command (focus, scroll, text, pane, native) when :command send_command(cmd.payload) # Batched widget-targeted commands when :commands send_commands(cmd.payload[:commands]) # Global widget operations (not targeted at a specific widget) when :widget_op (cmd.payload[:op], cmd.payload.except(:op)) # Typed font load message (carries raw TTF/OTF bytes) when :load_font send_load_font(cmd.payload[:family], cmd.payload[:data]) # Window operations when :window_op send_window_op(cmd.payload) when :window_query send_window_query(cmd.payload) when :system_op send_system_op(cmd.payload) when :system_query send_system_query(cmd.payload) # Effects when :effect execute_effect(cmd.payload) # Image operations when :image_op send_image_op(cmd.payload) # Test when :advance_frame send_advance_frame(cmd.payload[:timestamp]) else @logger.debug("plushie: unhandled command type: #{cmd.type}") end end |
#execute_done(value, mapper)
This method returns an undefined value.
Dispatch a done command immediately.
Guards against pathological update chains that keep returning
another Command.dispatch: the current chain position lives on
@dispatch_depth, the new event would be at depth + 1, and
past DISPATCH_DEPTH_LIMIT the runtime drops the command and
surfaces a typed DispatchLoopExceeded diagnostic.
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/plushie/runtime/commands.rb', line 146 def execute_done(value, mapper) next_depth = @dispatch_depth + 1 if next_depth > DISPATCH_DEPTH_LIMIT diag = Plushie::Event::Diagnostic::DispatchLoopExceeded.new( depth: next_depth, limit: DISPATCH_DEPTH_LIMIT ) = Plushie::Event::DiagnosticMessage.new( session: "", level: :error, diagnostic: diag ) @logger.error( "plushie: dispatch_loop_exceeded: command chain reached " \ "depth #{next_depth} (limit #{DISPATCH_DEPTH_LIMIT}); " \ "dropping command to break the loop" ) @diagnostics_mutex.synchronize { @diagnostics << } if @diagnostics return end event = mapper.call(value) enqueue_runtime_event([:dispatched_event, next_depth, event]) rescue => e @logger.warn("plushie: Command.done mapper error: #{e.class}: #{e.}") end |
#execute_effect(payload)
This method returns an undefined value.
Execute an effect request (send to renderer + start timeout).
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/plushie/runtime/commands.rb', line 208 def execute_effect(payload) id = payload[:id] tag = payload[:tag] kind = payload[:kind] opts = payload[:opts] || {} # One effect per tag: discard previous if same tag is in flight. if tag && (prev_id = @effect_tags[tag]) timer = @pending_effects.delete(prev_id) stop_thread(timer) @effect_ids.delete(prev_id) @effect_kinds.delete(prev_id) end # Track tag <-> wire ID mapping plus the effect kind (needed # by the response decoder to pick the right typed result). @effect_tags[tag] = id if tag @effect_ids[id] = tag @effect_kinds[id] = kind @bridge.send_encoded( Protocol::Encode.encode_effect(id, kind, opts, @format) ) # Start timeout timer timeout = payload[:timeout] || Effect.default_timeout(kind) queue = @event_queue timer = Thread.new do sleep(timeout / 1000.0) BoundedQueue.push(queue, [:effect_timeout, id]) end timer.name = "plushie-effect-timeout" @pending_effects[id] = timer end |
#execute_send_after(delay_ms, event)
This method returns an undefined value.
Schedule a delayed event. Uses a monotonic nonce to handle the Thread.kill race: if the old timer already pushed its event to the queue before being killed, the handler discards stale nonces.
192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/plushie/runtime/commands.rb', line 192 def execute_send_after(delay_ms, event) queue = @event_queue # Cancel existing timer for the same event key old_entry = @pending_timers[event] old_entry&.fetch(:thread)&.kill nonce = rand(1 << 64) thread = Thread.new do sleep(delay_ms / 1000.0) BoundedQueue.push(queue, [:send_after_event, event, nonce]) end thread.name = "plushie-timer" @pending_timers[event] = {thread: thread, nonce: nonce} end |
#execute_stream(callable, tag)
This method returns an undefined value.
Spawn a thread for streaming work with emit callback.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/plushie/runtime/commands.rb', line 109 def execute_stream(callable, tag) cancel_task(tag) nonce = rand(1 << 64) queue = @event_queue emit = ->(value) { BoundedQueue.push(queue, [:stream_value, tag, nonce, value]) } thread = Thread.new do result = callable.call(emit) BoundedQueue.push(queue, [:async_result, tag, nonce, result]) rescue => e BoundedQueue.push(queue, [:async_result, tag, nonce, [:error, e]]) end thread.name = "plushie-stream-#{tag}" @async_tasks[tag] = {thread: thread, nonce: nonce} end |
#send_advance_frame(timestamp)
This method returns an undefined value.
Advance the animation clock.
320 321 322 323 324 |
# File 'lib/plushie/runtime/commands.rb', line 320 def send_advance_frame() @bridge.send_encoded( Protocol::Encode.encode_advance_frame(, @format) ) end |
#send_command(payload)
This method returns an undefined value.
Send a single widget-targeted command via the unified wire format.
304 305 306 307 308 309 310 |
# File 'lib/plushie/runtime/commands.rb', line 304 def send_command(payload) @bridge.send_encoded( Protocol::Encode.encode_command( payload[:id], payload[:family], payload[:value], @format ) ) end |
#send_commands(commands)
This method returns an undefined value.
Send batched widget-targeted commands.
313 314 315 316 317 |
# File 'lib/plushie/runtime/commands.rb', line 313 def send_commands(commands) @bridge.send_encoded( Protocol::Encode.encode_commands(commands, @format) ) end |
#send_image_op(payload)
This method returns an undefined value.
Send an image operation.
297 298 299 300 301 |
# File 'lib/plushie/runtime/commands.rb', line 297 def send_image_op(payload) @bridge.send_encoded( Protocol::Encode.encode_image_op(payload[:op].to_s, payload, @format) ) end |
#send_load_font(family, data)
This method returns an undefined value.
Send a typed load_font message to the renderer.
251 252 253 254 255 |
# File 'lib/plushie/runtime/commands.rb', line 251 def send_load_font(family, data) @bridge.send_encoded( Protocol::Encode.encode_load_font(family, data, @format) ) end |
#send_system_op(payload)
This method returns an undefined value.
Send a system-level operation.
279 280 281 282 283 284 285 |
# File 'lib/plushie/runtime/commands.rb', line 279 def send_system_op(payload) op = payload[:op] settings = payload.except(:op) @bridge.send_encoded( Protocol::Encode.encode_system_op(op, settings, @format) ) end |
#send_system_query(payload)
This method returns an undefined value.
Send a system-level query.
288 289 290 291 292 293 294 |
# File 'lib/plushie/runtime/commands.rb', line 288 def send_system_query(payload) op = payload[:op] settings = payload.except(:op) @bridge.send_encoded( Protocol::Encode.encode_system_query(op, settings, @format) ) end |
#send_widget_op(op, payload)
This method returns an undefined value.
Send a widget operation to the renderer.
244 245 246 247 248 |
# File 'lib/plushie/runtime/commands.rb', line 244 def (op, payload) @bridge.send_encoded( Protocol::Encode.(op, payload, @format) ) end |
#send_window_op(payload)
This method returns an undefined value.
Send a window operation to the renderer.
258 259 260 261 262 263 264 265 |
# File 'lib/plushie/runtime/commands.rb', line 258 def send_window_op(payload) op = payload[:op] window_id = payload[:window_id] settings = payload.except(:op, :window_id) @bridge.send_encoded( Protocol::Encode.encode_window_op(op, window_id, settings, @format) ) end |
#send_window_query(payload)
This method returns an undefined value.
Send a window query (response arrives as effect_response or op_query_response).
268 269 270 271 272 273 274 275 276 |
# File 'lib/plushie/runtime/commands.rb', line 268 def send_window_query(payload) op = payload[:op] window_id = payload[:window_id] settings = payload.except(:op, :window_id, :tag) settings[:tag] = payload[:tag].to_s if payload[:tag] @bridge.send_encoded( Protocol::Encode.encode_window_op(op, window_id, settings, @format) ) end |
#stop_thread(thread, timeout: 0.5)
This method returns an undefined value.
100 101 102 103 104 105 106 |
# File 'lib/plushie/runtime/commands.rb', line 100 def stop_thread(thread, timeout: 0.5) return unless thread return if thread == Thread.current thread.kill thread.join(timeout) end |