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, server}`, where `ref` is a reference to the subscription and
`server` is the topic server PID (used for unsubscribing).
The `pid` will be sent messages, as described above.
## Options
* `params` - Optional map of params to pass to the topic (default: `%{}`).
These are merged with route params and passed to `connect/2` and `init/1`.
Topics with different param values are separate instances and do not share state.
## Example
Topical.subscribe(MyApp.Topical, ["lists", "foo"], self())
#=> {:ok, #Reference<0.4021726225.4145020932.239110>, #PID<0.123.0>}
"""
def subscribe(registry, topic, pid, context \\ nil, params \\ %{}) do
with {:ok, topic_key} <- Registry.resolve_topic(registry, topic, context, params),
{:ok, server} <- Registry.get_topic(registry, topic_key) do
# TODO: monitor/link server?
ref = GenServer.call(server, {:subscribe, pid, context})
{:ok, ref, server}
end
end
@doc """
Unsubscribes from a topic.
Takes the `server` PID and `ref` returned from `subscribe/5`.
## Example
{:ok, ref, server} = Topical.subscribe(MyApp.Topical, ["lists", "foo"], self())
Topical.unsubscribe(server, ref)
"""
def unsubscribe(server, ref) do
GenServer.cast(server, {:unsubscribe, ref})
end
@doc """
Captures the state of the `topic` (in the specified `registry`) without subscribing.
## Options
* `params` - Optional map of params to pass to the topic (default: `%{}`).
## Example
Topical.capture(MyApp.Topical, ["lists", "foo"])
# => {:ok, %{items: %{}, order: []}}
"""
def capture(registry, topic, context \\ nil, params \\ %{}) do
with {:ok, topic_key} <- Registry.resolve_topic(registry, topic, context, params),
{:ok, server} <- Registry.get_topic(registry, topic_key) do
{:ok, GenServer.call(server, {:capture, context})}
end
end
@doc """
Executes an action in a `topic`.
## Options
* `params` - Optional map of params to pass to the topic (default: `%{}`).
## Example
Topical.execute(MyApp.Topical, ["lists", "foo"], "add_item", {"Test", false})
#=> {:ok, "item123"}
"""
def execute(registry, topic, action, args \\ {}, context \\ nil, params \\ %{}) do
with {:ok, topic_key} <- Registry.resolve_topic(registry, topic, context, params),
{:ok, server} <- Registry.get_topic(registry, topic_key) 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.
## Options
* `params` - Optional map of params to pass to the topic (default: `%{}`).
## Example
Topical.notify(MyApp.Topical, ["lists", "foo"], "update_done", {"item123", true})
#=> :ok
"""
def notify(registry, topic, action, args \\ {}, context \\ nil, params \\ %{}) do
with {:ok, topic_key} <- Registry.resolve_topic(registry, topic, context, params),
{:ok, server} <- Registry.get_topic(registry, topic_key) do
GenServer.cast(server, {:notify, action, args, context})
end
end
end