Process-local, monitor-backed subscriptions to a Solve app.
Warm remote reads use the caller's cache without liveness or name-resolution RPCs. An owned app DOWN invalidates values and event routes but retains desired subscriptions. A lazy watcher restores them with capped exponential backoff. No controller state or actions are forwarded through the watcher.
Configure :lookup_timeout (default 5,000 ms) and :lookup_recovery
(initial_delay: 250, max_delay: 30_000, jitter: 0.2) under application :solve.
The delay cap is not an attempt limit. Healthy apps are not polled.
status/1 reports observed availability. Initial cold acquisition can exit;
reads while reconnecting exit immediately without additional network work.
Recovery invokes the ordinary data callback after fresh refs are installed.
The optional handle_solve_connection_changed/3 callback allows offline UI and
input gating without attempting to render unavailable data.
Distribution startup, credentials, command admission and protection against delayed transport delivery remain application responsibilities.
Summary
Functions
Drops cached refs and monitors for retired local app instances. No live subscriptions are removed.
Reads an instance-bound direct event tuple from a lookup item.
Consumes update/dispatch envelopes and owned lifecycle/recovery messages.
Reports the calling process's observed connection state, without resolving names,
polling processes, or performing network work. nil uses the current app context.
Releases a lookup interest using the calling process's :solve_app context.
Releases the calling process's cached interest in a target and requests raw detachment.
Types
Callbacks
@callback handle_solve_connection_changed(GenServer.server(), connection_status(), term()) :: {:ok, term()}
Functions
@spec cleanup() :: :ok
Drops cached refs and monitors for retired local app instances. No live subscriptions are removed.
@spec collection(atom()) :: Solve.Collection.t(map())
@spec collection(GenServer.server() | nil, atom()) :: Solve.Collection.t(map())
@spec dispatch(dispatch_event()) :: :ok
@spec dispatch(target() | dispatch_event(), term()) :: :ok
@spec dispatch(GenServer.server() | nil, target(), atom(), term()) :: :ok
@spec event(map() | nil, atom()) :: dispatch_event() | nil
Reads an instance-bound direct event tuple from a lookup item.
@spec event(map() | nil, atom(), term()) :: dispatch_event() | nil
@spec handle_message( Solve.Message.t() | {:solve_lookup_down | :solve_lookup_watcher_down, reference(), :process, pid(), term()} | {:solve_lookup, atom(), term()} ) :: map()
Consumes update/dispatch envelopes and owned lifecycle/recovery messages.
Manual/helpers consumers must forward %Solve.Message{}, both tagged DOWN forms
(:solve_lookup_down, :solve_lookup_watcher_down), and
{:solve_lookup, kind, payload} messages. Foreign monitor refs are ignored.
Successful recovery returns the same grouped Updated data as normal updates.
Use status/1 to inspect availability in a manually wired consumer.
Acquire a target with solve/2 or collection/2 before forwarding its updates.
Only versioned updates carrying the canonical app PID for an existing ref are
accepted. Unsolicited, versionless, obsolete, and retired-app updates are ignored;
messages never create subscriptions or seed the cache. Forward the complete runtime
envelope rather than rebuilding an update from its value.
@spec solve(GenServer.server() | nil, target()) :: map() | nil
@spec status(GenServer.server() | nil) :: :unknown | {:connected, pid()} | {:reconnecting, term()} | {:unavailable, term()}
Reports the calling process's observed connection state, without resolving names,
polling processes, or performing network work. nil uses the current app context.
{:connected, pid} means this address's wanted set is installed and no failure has
been consumed. It is not an instantaneous health check: a silent network stall
can precede Erlang's failure detection. :unknown means no retained interest through that address.
Named apps automatically reconnect; a confirmed dead explicit PID is unavailable
and cannot follow a replacement. Unsubscribe to cancel or deliberately reacquire.
Releases a lookup interest using the calling process's :solve_app context.
See unsubscribe/2 for ownership and partial-failure semantics.
@spec unsubscribe(GenServer.server() | nil, target()) :: :ok | {:error, term()}
Releases the calling process's cached interest in a target and requests raw detachment.
Use an app PID or a name previously used to acquire a lookup. Names identify their
cached app instance: this function never resolves a name to discover a replacement
app. Pending recovery intent is canceled even when no active ref remains. An
unknown alias or missing interest returns :ok without an app call. Passing
nil uses the calling process's :solve_app context.
Removes only this target. Collection sources and individual items are independent;
repeated reads and aliases share one interest, not reference counts. The last ref
for an app also releases its lookup monitor and aliases. Queued updates cannot
recreate a removed ref; a later solve/2 or collection/2 explicitly reacquires it.
No update callback is invoked and previously returned event tuples remain usable.
Cancellation also removes equivalent recovery intent and fences queued retries;
releasing the last pending interest stops its watcher. Raw Solve.unsubscribe
alone does not cancel Lookup intent. Partial recovery refs use the same pinned
raw-detachment semantics as active refs.
A raw reentrancy error preserves the cache unchanged. Other raw errors remove the ref but leave physical detachment unconfirmed. Outer app-call exits also remove the ref before propagating; confirmed app death is treated as successful cleanup.
:ok for a missing ref certifies only a local no-op, not physical detachment. After
a timeout, another lookup unsubscribe does not retry the raw operation. Use
Solve.unsubscribe/2 with the original app PID if confirmation is needed, or to
release raw subscriptions that have no lookup ref (including failed acquisitions).