Skip to main content

lib/bb/jido/action/wait_for_state.ex

# SPDX-FileCopyrightText: 2026 James Harton
#
# SPDX-License-Identifier: Apache-2.0

defmodule BB.Jido.Action.WaitForState do
  @moduledoc """
  Jido action that waits for a Beam Bots robot to enter a target state.

  Subscribes to the `[:state_machine]` PubSub topic and blocks until the
  robot reports a transition into `:target`, or returns immediately if the
  robot is already in that state.

  ## Schema

  - `:robot` — the robot module (required).
  - `:target` — the desired robot state atom (required, e.g. `:idle`,
    `:armed`).
  - `:timeout` — millisecond timeout (default `30_000`).

  ## Returns

  - `{:ok, %{robot: ..., state: target}}` when the state is reached.
  - `{:error, :timeout}` if the timeout elapses first.

  ## Warning

  This action blocks the calling process while waiting. When invoked
  directly from a Jido agent it will block the agent server; prefer
  running it from a dedicated process or via a workflow when long waits
  are expected.
  """

  use Jido.Action,
    name: "bb_wait_for_state",
    description: "Wait for a Beam Bots robot to enter a target state",
    schema: [
      robot: [type: :atom, required: true, doc: "Robot module"],
      target: [type: :atom, required: true, doc: "Desired robot state"],
      timeout: [
        type: :pos_integer,
        default: 30_000,
        doc: "Wait timeout in milliseconds"
      ]
    ]

  alias BB.Message
  alias BB.Robot.Runtime
  alias BB.StateMachine.Transition

  @impl Jido.Action
  def run(%{robot: robot, target: target} = params, _context) do
    timeout = Map.get(params, :timeout, 30_000)

    case Runtime.state(robot) do
      ^target ->
        {:ok, %{robot: robot, state: target}}

      _other ->
        wait_for_transition(robot, target, timeout)
    end
  end

  defp wait_for_transition(robot, target, timeout) do
    case BB.PubSub.subscribe(robot, [:state_machine], message_types: [Transition]) do
      {:ok, _pid} ->
        try do
          receive_transition(robot, target, timeout)
        after
          BB.PubSub.unsubscribe(robot, [:state_machine])
        end

      {:error, reason} ->
        {:error, {:subscribe_failed, reason}}
    end
  end

  defp receive_transition(robot, target, timeout) do
    receive do
      {:bb, [:state_machine], %Message{payload: %Transition{to: ^target}}} ->
        {:ok, %{robot: robot, state: target}}

      {:bb, [:state_machine], %Message{payload: %Transition{}}} ->
        receive_transition(robot, target, timeout)
    after
      timeout ->
        {:error, :timeout}
    end
  end
end