Core lifecycle GenServer for Plushie applications.
The runtime is the heartbeat of a Plushie app. It owns the Elm-style update loop: event in -> model out -> view out -> snapshot to bridge.
Startup
On init/1 the runtime:
- Calls
app.init(app_opts)to get the initial model (and optional commands). - Calls
app.view(model)to produce the initial UI tree. - Normalizes the tree via
Plushie.Tree.normalize/1. - Sends a full snapshot to the bridge via
Plushie.Bridge.send_snapshot/2. - Executes any commands returned from
init/1.
Event loop
On every {:renderer_event, event}:
- Calls
app.update(model, event). - Executes returned commands.
- Calls
app.view(model)on the new model. - Diffs against the previous tree; sends a patch if changed, or a full snapshot on first render / after renderer restart.
State shape
%{
app: module(),
model: term(),
bridge: pid() | atom(),
daemon: boolean(),
tree: map() | nil,
subscriptions: %{term() => {:timer, reference()} | {:renderer, atom(), non_neg_integer() | nil}},
subscription_keys: [term()],
windows: MapSet.t(),
async_tasks: %{atom() => {pid(), reference()}},
pending_effects: %{String.t() => %{tag: atom(), timer_ref: reference()}},
pending_timers: %{term() => reference()},
pending_coalesce: %{term() => Plushie.Event.t()},
pending_coalesce_order: [term()],
coalesce_timer: reference() | nil,
consecutive_errors: non_neg_integer(),
pending_interact: {GenServer.from(), String.t(), reference()} | nil
}Exit trapping
The runtime traps exits so a bridge crash does not silently kill it.
Summary
Functions
Waits for an async task with the given tag to complete.
Returns a specification to start this module under a supervisor.
Dispatches a message through app.update/2, then re-renders.
Finds a node in the current tree by exact scoped ID.
Finds a node in the current tree by exact scoped ID inside a specific window.
Finds a node in the current tree using a predicate function.
Returns the bridge pid for this runtime.
Returns and clears accumulated prop validation diagnostics.
Returns the current app model synchronously.
Returns the current normalized UI tree synchronously.
Performs a synchronous interact via the renderer.
Registers an effect stub with the renderer.
Starts the runtime linked to the calling process.
Waits for the runtime to finish processing all pending messages.
Removes a previously registered effect stub.
Functions
@spec await_async(GenServer.server(), atom(), timeout()) :: :ok | {:error, :await_in_progress}
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/2.
Returns {:error, :await_in_progress} if another caller is already
waiting for the same tag.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec dispatch(GenServer.server(), term()) :: :ok
Dispatches a message through app.update/2, then re-renders.
Fire-and-forget from the caller's perspective. The runtime processes the message asynchronously. Use this to send results from spawned processes back to the runtime:
runtime = self() # inside update/2, self() is the runtime
spawn(fn ->
result = expensive_computation()
Plushie.Runtime.dispatch(runtime, {:computation_done, result})
end)
# In update/2:
def update(model, {:computation_done, result}), do: ...Prefer Plushie.Command.async/2 for most async work. Use dispatch/2
when you need direct control over the spawned process lifecycle.
@spec find_node(GenServer.server(), String.t()) :: map() | nil
Finds a node in the current tree by exact scoped ID.
@spec find_node(GenServer.server(), String.t(), String.t()) :: map() | nil
Finds a node in the current tree by exact scoped ID inside a specific window.
@spec find_node_by(GenServer.server(), (map() -> boolean())) :: map() | nil
Finds a node in the current tree using a predicate function.
@spec get_bridge(GenServer.server()) :: pid() | atom() | nil
Returns the bridge pid for this runtime.
@spec get_diagnostics(GenServer.server()) :: [Plushie.Event.SystemEvent.t()]
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/2)
and accumulated in state. This function atomically retrieves and
clears the list.
@spec get_model(GenServer.server()) :: term()
Returns the current app model synchronously.
@spec get_tree(GenServer.server()) :: map() | nil
Returns the current normalized UI tree synchronously.
@spec interact(GenServer.server(), String.t(), map(), map(), timeout()) :: :ok | {:error, :interact_in_progress | :renderer_restarted | {:renderer_exit, term()}}
Performs a synchronous interact via the renderer.
Sends an interact request (e.g. click, type_text) to the renderer, which
processes it against its widget tree and sends back events. The runtime
processes those events through update/2 and re-renders. Blocks until
the renderer signals completion. Returns an error if another interact is
already in flight, or if the renderer exits or restarts before the
interaction finishes.
@spec register_effect_stub( GenServer.server(), Plushie.Effect.kind(), term(), timeout() ) :: :ok | {:error, :stub_ack_pending}
Registers an effect stub with the renderer.
The renderer will return response immediately for any effect of
the given kind, without executing the real effect. Blocks until
the renderer confirms the stub is stored.
The kind matches the effect function name as an atom (e.g.
:file_open, :clipboard_write).
Returns {:error, :stub_ack_pending} if a register or unregister
for the same kind is already awaiting confirmation.
@spec start_link(keyword()) :: GenServer.on_start()
Starts the runtime linked to the calling process.
Required opts:
:app- module implementingPlushie.App:bridge- pid or registered name of thePlushie.BridgeGenServer
Optional opts:
:name- registration name passed toGenServer.start_link/3
Any other opts are forwarded to app.init/1 as the app opts keyword list.
@spec sync(runtime :: GenServer.server()) :: :ok
Waits for the runtime to finish processing all pending messages.
Returns :ok once the runtime is idle. Use this to synchronize after
dispatching events or starting the runtime, ensuring init/update
cycles have completed before inspecting state.
@spec unregister_effect_stub(GenServer.server(), Plushie.Effect.kind(), timeout()) :: :ok | {:error, :stub_ack_pending}
Removes a previously registered effect stub.
Blocks until the renderer confirms the stub is removed.
Returns {:error, :stub_ack_pending} if a register or unregister
for the same kind is already awaiting confirmation.