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.dispatch chain depth before the runtime guard fires. Command.dispatch queues a follow-up event back through the runtime mailbox; a pathological update that keeps returning another dispatch would fill the queue indefinitely, so the runtime caps the chain and surfaces a typed DispatchLoopExceeded diagnostic.

Returns:

  • (Integer)
100

Instance Method Summary collapse

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.

Parameters:

  • tag (Symbol)


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

Parameters:

  • message (Object)

Returns:

  • (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(message)
  unless Thread.current == @runtime_thread
    return BoundedQueue.push(@event_queue, message)
  end

  queued = BoundedQueue.try_push(@event_queue, message)
  return queued if queued

  @pending_runtime_events << {
    message: message,
    remaining: @event_queue.length
  }
  message
end

#execute_async(callable, tag)

This method returns an undefined value.

Spawn a dedicated thread for async work with nonce tracking.

Parameters:

  • callable (Proc)
  • tag (Symbol)


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.

Parameters:



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
    send_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.

Parameters:

  • value (Object)
  • mapper (Proc)


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
    )
    message = 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 << message } 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.message}")
end

#execute_effect(payload)

This method returns an undefined value.

Execute an effect request (send to renderer + start timeout).

Parameters:

  • payload (Hash[Symbol, untyped])


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.

Parameters:

  • delay_ms (Integer)
  • event (Object)


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.

Parameters:

  • callable (Proc)
  • tag (Symbol)


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.

Parameters:

  • timestamp (Integer)


320
321
322
323
324
# File 'lib/plushie/runtime/commands.rb', line 320

def send_advance_frame(timestamp)
  @bridge.send_encoded(
    Protocol::Encode.encode_advance_frame(timestamp, @format)
  )
end

#send_command(payload)

This method returns an undefined value.

Send a single widget-targeted command via the unified wire format.

Parameters:

  • payload (Hash[Symbol, untyped])


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.

Parameters:

  • commands (Array[Hash[Symbol, untyped]])


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.

Parameters:

  • payload (Hash[Symbol, untyped])


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.

Parameters:

  • family (String)
  • data (String)


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.

Parameters:

  • payload (Hash[Symbol, untyped])


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.

Parameters:

  • payload (Hash[Symbol, untyped])


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.

Parameters:

  • op (Symbol)
  • payload (Hash[Symbol, untyped])


244
245
246
247
248
# File 'lib/plushie/runtime/commands.rb', line 244

def send_widget_op(op, payload)
  @bridge.send_encoded(
    Protocol::Encode.encode_widget_op(op, payload, @format)
  )
end

#send_window_op(payload)

This method returns an undefined value.

Send a window operation to the renderer.

Parameters:

  • payload (Hash[Symbol, untyped])


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).

Parameters:

  • payload (Hash[Symbol, untyped])


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.

Parameters:

  • thread (Thread, nil)
  • timeout: (Numeric) (defaults to: 0.5)


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