# `Issho.State`
[🔗](https://github.com/hattoriai/issho/blob/v0.4.0/lib/issho/state.ex#L1)

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
       }}
    end

Then, 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.

# `op`

```elixir
@type op() :: map()
```

# `path`

```elixir
@type path() :: [String.t() | atom() | integer()]
```

# `assign`

```elixir
@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` (`%{}`).

# `change`

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

# `delete`

```elixir
@spec delete(path()) :: op()
```

Deletes the value at a path: a key of a map, or an item of a list.

# `get`

```elixir
@spec get(map() | nil, path()) :: term()
```

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

# `increment`

```elixir
@spec increment(path(), number()) :: op()
```

Adds to the number at a path (`1` unless given); a missing one is `0`.

# `insert`

```elixir
@spec insert(path(), map(), keyword()) :: op()
```

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"}

# `move`

```elixir
@spec move(path(), String.t(), keyword()) :: op()
```

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"}

# `remove`

```elixir
@spec remove(path(), String.t()) :: op()
```

Removes the item `id` from the list at a path.

# `set`

```elixir
@spec set(path(), term()) :: op()
```

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"}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
