Coordinates controller graph validation, lifecycle management, and subscriptions.
Explicit app dispatch requires dispatch/4, including a payload (or %{}).
The implicit dispatch/2 and dispatch/3 variants use controller callback context.
Apps own a temporary-child supervisor. start_link/1 accepts :name, :params,
and a positive :controller_start_timeout in milliseconds (default 5,000).
Controller shutdown is bounded to 1,000 ms.
Summary
Functions
Subscribes a PID and returns raw exposed state, a collection, or nil.
Removes a PID's raw subscription to a singleton, collection source, or collection item.
Types
@type controller_name() :: Solve.ControllerSpec.name()
@type controller_status() :: :started | :stopped
@type controller_target() :: controller_name() | {controller_name(), Solve.Collection.id()}
@type graph() :: [Solve.ControllerSpec.t()]
@type runtime_state() :: map()
Callbacks
@callback controllers() :: graph()
Functions
@spec controller_events(GenServer.server(), controller_target()) :: [atom()] | nil
@spec controller_pid(GenServer.server(), controller_target()) :: pid() | nil
@spec controller_variant(GenServer.server(), controller_name()) :: Solve.ControllerSpec.variant() | nil
@spec dispatch(controller_target(), atom()) :: :ok
@spec dispatch(controller_target(), atom(), term()) :: :ok
@spec dispatch(GenServer.server(), controller_target(), term(), term()) :: :ok
@spec subscribe(GenServer.server(), controller_target(), pid()) :: term()
Subscribes a PID and returns raw exposed state, a collection, or nil.
If a live controller's subscription handshake times out, returns its last accepted cached snapshot and schedules bounded attachment retries without restarting it.
@spec unsubscribe(GenServer.server(), controller_target(), pid()) :: :ok | {:error, :timeout | :reentrant_unsubscribe | {:unsubscribe_failed, term()}}
Removes a PID's raw subscription to a singleton, collection source, or collection item.
Subscriptions are not reference counted. Repeating this call is idempotent unless
another subscribe intervenes. Source and item subscriptions are independent.
This does not stop the controller, remove graph dependencies, revoke event handles,
or clear Solve.Lookup caches. Warm lookup reads will not resubscribe automatically.
On :ok, explicit interest and reattachment retries are removed and any live target
has acknowledged external detachment. Exception: when subscriber is the app PID,
its mandatory internal observer stays attached and continues receiving updates.
Already-sent or queued messages may still arrive; no final nil update is sent.
A controller calling this for its own target would block its own acknowledgement.
Such calls return {:error, :reentrant_unsubscribe} without changing state, unless
only explicit app-PID interest is being removed. Indirect synchronous callback
cycles can still time out. Calls from the app's own params/collect callbacks retain
normal GenServer.call/2 self-call restrictions.
If the controller does not acknowledge within one second, returns
{:error, :timeout} with logical interest removed but physical detachment unconfirmed.
Other handshake failures return {:error, {:unsubscribe_failed, reason}} with the
same logical effect. These failures do not restart the controller.
The outer app call retains normal GenServer.call/2 exits, including its default
five-second timeout. After an outer timeout, even logical removal is unconfirmed.
Neither timeout cancels an already-sent request; it may execute later.
Retry only while the intended state is still unsubscribed: a new call removes any newer subscription for that PID/target. Names resolve to the current app instance; use the original app PID if a retry must not affect a replacement app.