lib/topical.ex

defmodule Topical do
  @moduledoc """
  This module provides the high level interface for interacting with topics. Primarily for
  subscribing (and unsubscribing), but also for sending requests.

  After subscribing, a client will initially receive a `{:reset, ref, value}` message, and then
  subsequent `{:updates, ref, updates}` messages when the value of the topic changes, where
  `updates` is a list with each item being one of:

   - `{:set, path, value}`: the `value` has been set at the `path`.
   - `{:unset, path, key}`: the `key` has been unset from the object at the `path`.
   - `{:insert, path, index, values}`: the `values` have been inserted into the array at the `path`.
   - `{:delete, path, index, count}`: `count` values have been deleted from the array at the `path`, from the position `index`.

  A client can interact directly with a topic by _executing_ actions (which returns a result), or
  by _notifying_ (without waiting for a result). These are analogous to `GenServer.call/3` and
  `GenServer.cast/2`. Be aware that a topic is blocked while processing a request.
  """

  alias Topical.Registry

  @doc """
  Returns a specification to start a Topical registry under a supervisor.
  """
  def child_spec(options) do
    %{
      id: Keyword.get(options, :server, Topical),
      start: {Registry, :start_link, [options]},
      type: :supervisor
    }
  end

  @doc """
  Subscribes to the specified `topic` (in the specified `registry`).

  Returns `{:ok, ref}`, where the `ref` is a reference to the subscription.

  The `pid` will be send messages, as described above.

  ## Example

      Topical.subscribe(MyApp.Topical, ["lists", "foo"], self())
      #=> {:ok, #Reference<0.4021726225.4145020932.239110>}

  """
  def subscribe(registry, topic, pid, context \\ nil) do
    with {:ok, server} <- Registry.get_topic(registry, topic) do
      # TODO: monitor/link server?
      {:ok, GenServer.call(server, {:subscribe, pid, context})}
    end
  end

  @doc """
  Unsubscribes from a `topic` (in the specified `registry`).

  ## Example

      Topical.unsubscribe(MyApp.Topical, ["lists", "foo"], ref)

  """
  def unsubscribe(registry, topic, ref) do
    # TODO: don't start server if not running
    with {:ok, server} <- Registry.get_topic(registry, topic) do
      GenServer.cast(server, {:unsubscribe, ref})
    end
  end

  @doc """
  Captures the state of the `topic` (in the specified `registry`) without subscribing.

  ## Example

      Topical.capture(MyApp.Topical, ["lists", "foo"])
      # => {:ok, %{items: %{}, order: []}}
  """
  def capture(registry, topic, context \\ nil) do
    with {:ok, server} <- Registry.get_topic(registry, topic) do
      {:ok, GenServer.call(server, {:capture, context})}
    end
  end

  @doc """
  Executes an action in a `topic`.

  ## Example

      Topical.execute(MyApp.Topical, ["lists", "foo"], "add_item", {"Test", false})
      #=> {:ok, "item123"}

  """
  def execute(registry, topic, action, args \\ {}, context \\ nil) do
    with {:ok, server} <- Registry.get_topic(registry, topic) do
      {:ok, GenServer.call(server, {:execute, action, args, context})}
    end
  end

  @doc """
  Send a notification to a registry.

  This is similar to `execute/4`, except no result is waited for.

  ## Example

      Topical.notify(MyApp.Topical, ["lists", "foo"], "update_done", {"item123", true})
      #=> :ok

  """
  def notify(registry, topic, action, args \\ {}, context \\ nil) do
    with {:ok, server} <- Registry.get_topic(registry, topic) do
      GenServer.cast(server, {:notify, action, args, context})
    end
  end
end