Shared state without an engine of your own: a map that people and
agents change together (Issho.Engine.Map), as a LiveView assign that
stays up to date. A retro board, a poll, a checklist, a kanban's order,
a game's turn.
Define the document in your policy, as any document, with its initial
content from your database and an on_transaction hook that checks a
change and saves it (see Issho.Documents.open/4):
def document(_who, "retro:" <> id = doc) do
{:ok,
%{
engine: Issho.Engine.Map,
initial: Retros.get!(id).state,
on_transaction: fn _ops, %{after: state} -> Retros.save(id, state) end
}}
endThen, in a LiveView (with on_mount Issho.Live):
def mount(%{"id" => id}, _session, socket) do
room = "retro:" <> id
if connected?(socket),
do: {:ok, _} = Issho.Live.join(MyApp.Issho, socket.assigns.current_user, room)
{:ok, socket |> assign(:room, room) |> Issho.State.assign(:retro, room)}
end
def handle_event("add", %{"text" => text}, socket) do
card = %{"id" => Ecto.UUID.generate(), "text" => text, "votes" => 0}
:ok = Issho.State.change(socket, :retro, [Issho.State.insert(["cards"], card)])
{:noreply, socket}
end
def handle_event("vote", %{"id" => id}, socket) do
:ok = Issho.State.change(socket, :retro, [Issho.State.increment(["cards", id, "votes"])])
{:noreply, socket}
end@retro is the state, a map, and changes as anyone changes it: this
LiveView, another person's, an agent (the update_state tool), or your
server (Issho.transact/4). It is the state the authority accepted, in
its order, the same in every LiveView of the room.
Operations
set/2, delete/1, increment/2, and, for lists of items by id (the
cards of a column), insert/3, move/3 and remove/2. A path is a
list of keys, and of items' ids through a list: ["columns", "todo", card_id, "title"]. Concurrent changes merge as Issho.Engine.Map
says: two people who add a card at once both have theirs.
Summary
Functions
Assigns a document of the room as key, kept up to date as it
changes: the LiveView must be in the room (Issho.Live.join/4), and the
document's engine Issho.Engine.Map. In the static render, before the
LiveView is connected and in the room, key is :default (%{}).
Deletes the value at a path: a key of a map, or an item of a list.
The value at a path of a state, through lists by items' ids, or nil.
Adds to the number at a path (1 unless given); a missing one is 0.
Inserts an item (a map with an "id") in the list at a path. Options:
:after or :before an item's id, or :at (:start or :end, the
end unless given).
Moves the item id of the list at a path, within it or to the list at
:to, placed :after or :before an item's id, or :at (:start or
:end). An :after or :before of nil is not given.
Removes the item id from the list at a path.
Sets the value at a path, making the maps on the way.
Types
Functions
@spec assign( Phoenix.LiveView.Socket.t(), atom(), String.t(), String.t() | nil, keyword() ) :: Phoenix.LiveView.Socket.t()
Assigns a document of the room as key, kept up to date as it
changes: the LiveView must be in the room (Issho.Live.join/4), and the
document's engine Issho.Engine.Map. In the static render, before the
LiveView is connected and in the room, key is :default (%{}).
@spec change(Phoenix.LiveView.Socket.t(), atom(), [op()]) :: :ok | {:error, term()}
Changes the state assigned as key, with operations (set/2,
insert/3...), as one transaction: all of them, or none. The assign
changes when the room's authority accepts it, as everyone's does.
Gives :ok, or {:error, reason} (your hook refused it, the policy, an
operation).
Deletes the value at a path: a key of a map, or an item of a list.
The value at a path of a state, through lists by items' ids, or nil.
iex> state = %{"cols" => %{"todo" => [%{"id" => "c1", "title" => "Plan"}]}}
iex> Issho.State.get(state, ["cols", "todo", "c1", "title"])
"Plan"
iex> Issho.State.get(state, ["cols", "done"])
nil
Adds to the number at a path (1 unless given); a missing one is 0.
Inserts an item (a map with an "id") in the list at a path. Options:
:after or :before an item's id, or :at (:start or :end, the
end unless given).
iex> Issho.State.insert(["cards"], %{"id" => "c2"}, after: "c1")
%{"op" => "insert", "path" => ["cards"], "value" => %{"id" => "c2"}, "after" => "c1"}
Moves the item id of the list at a path, within it or to the list at
:to, placed :after or :before an item's id, or :at (:start or
:end). An :after or :before of nil is not given.
iex> Issho.State.move(["cols", "todo"], "c1", to: ["cols", "done"], at: :start)
%{"op" => "move", "path" => ["cols", "todo"], "id" => "c1", "to" => ["cols", "done"], "at" => "start"}
Removes the item id from the list at a path.
Sets the value at a path, making the maps on the way.
iex> Issho.State.set(["plan", :title], "Q3")
%{"op" => "set", "path" => ["plan", "title"], "value" => "Q3"}