Solve.Lookup behaviour (Solve v0.3.0)

Copy Markdown View Source

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

connection_status()

@type connection_status() ::
  :unknown
  | {:connected, pid()}
  | {:reconnecting, term()}
  | {:unavailable, term()}

dispatch_event()

@type dispatch_event() ::
  {pid(), {:solve_event, atom()}} | {pid(), {:solve_event, atom(), term()}}

target()

@type target() :: Solve.controller_target()

Callbacks

handle_solve_connection_changed(server, connection_status, term)

(optional)
@callback handle_solve_connection_changed(GenServer.server(), connection_status(), term()) ::
  {:ok, term()}

handle_solve_updated(map, term)

(optional)
@callback handle_solve_updated(map(), term()) :: {:ok, term()}

Functions

cleanup()

@spec cleanup() :: :ok

Drops cached refs and monitors for retired local app instances. No live subscriptions are removed.

collection(source)

@spec collection(atom()) :: Solve.Collection.t(map())

collection(app, source)

@spec collection(GenServer.server() | nil, atom()) :: Solve.Collection.t(map())

dispatch(arg)

@spec dispatch(dispatch_event()) :: :ok

dispatch(target, payload)

@spec dispatch(target() | dispatch_event(), term()) :: :ok

dispatch(target, event, payload)

@spec dispatch(target(), atom(), term()) :: :ok

dispatch(app, target, event, payload)

@spec dispatch(GenServer.server() | nil, target(), atom(), term()) :: :ok

event(controller, event_name)

@spec event(map() | nil, atom()) :: dispatch_event() | nil

Reads an instance-bound direct event tuple from a lookup item.

event(controller, event_name, payload)

@spec event(map() | nil, atom(), term()) :: dispatch_event() | nil

events(arg1)

@spec events(map() | nil) :: map() | nil

handle_message(arg1)

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

solve(target)

@spec solve(target()) :: map() | nil

solve(app, target)

@spec solve(GenServer.server() | nil, target()) :: map() | nil

status(app)

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

unsubscribe(target)

@spec unsubscribe(target()) :: :ok | {:error, term()}

Releases a lookup interest using the calling process's :solve_app context.

See unsubscribe/2 for ownership and partial-failure semantics.

unsubscribe(app, target)

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