defmodule OT.Server do @moduledoc """ A safe API for interacting with operations and the data they operate against. """ use GenServer @typedoc """ A map containing OT-related information. This map must contain at least three keys: - `type`: A string representing the OT type, which will be used to find the appropriate OT module. - `version`: A non-negative integer representing the current `t:version/0` of the datum. - `content`: The contents of the datum that `t:operation/0`s will be applied to. """ @type datum :: %{type: String.t, version: non_neg_integer, content: any} @typedoc """ A piece of information that can uniquely identify a `t:datum/0`. """ @type datum_id :: any @typedoc """ A list of units of work performed against a single piece of data (a `t:datum/0`). """ @type operation :: [any] @typedoc """ A non-negative integer representing an operation or `t:datum/0` version. """ @type version :: non_neg_integer @typedoc """ A tuple representing an `t:operation/0` and its `t:version/0`. """ @type operation_info :: {operation, version} @doc false def start_link(_) do GenServer.start_link(__MODULE__, []) end @doc """ Submit an operation. ## Example iex> :ets.insert(:ot_data, ...> {"id", %{id: "id", content: "Hllo, ", type: "text", version: 0}}) iex> OT.Server.submit_operation("id", {[1, %{i: "e"}], 1}) iex> OT.Server.submit_operation("id", {[6, %{i: "world."}], 1}) {:ok, {[7, %{i: "world."}], 2}} iex> OT.Server.get_datum("id") {:ok, %{id: "id", content: "Hello, world.", type: "text", version: 2}} If the operation succeeds, a tuple will be returned with the operation and its version. Otherwise, an error will be returned. """ @spec submit_operation(any, {OT.Operation.t, pos_integer}, any) :: {:ok, {OT.Operation.t, pos_integer}} | {:error, any} def submit_operation(datum_id, {op, vsn}, meta \\ nil) do call_with_worker({:submit_operation, datum_id, {op, vsn}, meta}) end @doc """ Get a datum. This will call the configured adapter's `c:OT.Server.Adapter.get_datum/1` function and return that value. ## Example iex> :ets.insert(:ot_data, {"id", %{id: "id"}}) iex> OT.Server.get_datum("id") {:ok, %{id: "id"}} If the datum is found, it will be returned. Otherwise, an error is returned. Also, note that this function does get called in a worker, so shares worker bandwidth with `submit_operation/3`. """ @spec get_datum(any) :: {:ok, any} | {:error, any} def get_datum(id) do call_with_worker({:get_datum, id}) end @impl true def handle_call(call_args, _from, state) do [command | args] = Tuple.to_list(call_args) result = apply(OT.Server.Impl, command, args) {:reply, result, state} end defp call_with_worker(call_args) do :poolboy.transaction(:ot_worker, fn ot_worker -> GenServer.call(ot_worker, call_args) end) end end