README.md

# PhoenixLiveState

PhoenixLiveState enables LiveView processes to share reactive state across a
cluster of nodes. Attach LiveViews to a state server and get automatic assign
synchronization.

## Features
- Shared state per arbitrary key (room id, dashboard id, etc), across a cluster of nodes.
- Automatic updates delivered as `@live_assigns` to be used in templates.
- Simple macros for declaring a state spec and LiveView clients
- ACL-based permission model (group vs public)
- TTL-based auto-shutdown when no clients remain
- Direct server API usable from non-LiveView processes

## Installation

Add to `mix.exs`:

```elixir
def deps do
  [
    {:phoenix_live_state, "~> 0.1.0"}
  ]
end
```

Add the supervisor in your application tree, BEFORE your endpoint supervisor!

```elixir
defmodule MyApp.Application do
  ...
  def start(_type, _args) do
    children = [
      ...,
      PhoenixLiveState.Supervisor,
      MyApp.Endpoint
    ]
    ...
  end
  ...
end
```

## Usage Example

### Define a State Spec

The state spec defines the initial state, permissions and any other settings
for your shared state. This function is called when the state server is started
and when it's reset.

```elixir
defmodule MyAppWeb.ChatRoomState do
  use PhoenixLiveState, :state

  @impl true
  def on_mount(room_id) do
    {:ok,
      initial_state: [messages: []]
    }
  end
end
```

### Use in a LiveView

In you LiveView, you use `PhoenixLiveState, client: opts` to inject the hooks
as well as the helper functions: `live_state/3` and `live_assign/2`

On the `mount/3` function, call `live_state/3` to attach your liveview to the
state server. From there, it will inject a new assign, `@live_assigns` in your
socket.

That's it. Now, whenever the state server is updated, all connected LiveViews
will receive the updated state!

```elixir
defmodule MyAppWeb.ChatRoomLive do
  use MyAppWeb, :live_view
  use PhoenixLiveState, client: [
    live_state: MyAppWeb.ChatRoomState
  ]

  def mount(%{"id" => room_id}, _session, socket) do
    {:ok, live_state(socket, room_id)}
  end

  def handle_event("submit", %{"message" => new_msg}, socket) do
    msgs = socket.assigns.live_assigns.messages
    {:noreply, socket |> live_assign(:messages, [new_msg, msgs]) |> assign(...)}
  end

  def render(assigns) do
    ~H"""
    <div>
      <div :for={message <- @live_assigns.messages}>
        {message}
      </div>
    </div>
    <div>
      <.form :for={%{}} phx-submit="submit">
        <.input name="message" value="">
      </.form>
    </div>
    """
  end
end
```

## Server API

You may also interact with the shared state server directly, as long as you know
the server's key. This is useful to integrate with non-liveview processes, like
a Phoenix Controller:

```elixir
PhoenixLiveState.StateServer.get(key)              # {:ok, full_state}
PhoenixLiveState.StateServer.get(key, :messages)   # {:ok, value} | {:error, :property_not_found}
PhoenixLiveState.StateServer.put(key, :users, %{})
PhoenixLiveState.StateServer.delete(key, :typing)
PhoenixLiveState.StateServer.reset(key)            # reruns on_mount/1
```

## ACL
Defines access control list (ACL) for the state server. Clients are considered:
- group: if they called `StateServer.attach/1`
- public: all other processes.

To each type of client, you may have the following permissions:
- `:read` - can read the state of the server
- `:write` - can write into the state of the server
- `:delete` - can delete the state of the server
- `:reset` - can reset the state of the server

You may use th alias `:all` to indicate that the client type has full access to
the server.

#### Examples

```elixir
%{
  group: :all,    # attached clients
  public: [:read] # other callers
}
```


## Lifecycle
1. First `live_state/2` starts a state server for the key (if absent).
2. Clients attach; each gets full state as `@live_assigns`.
3. Writes (`live_assign/3` or server API) broadcast diffs (new|updated|deleted|reset).
4. When last client detaches a TTL countdown begins (default 60s).
5. Server exits if no new client attaches before TTL expires.

## License
MIT © 2025 Marcos da Silva Ramos

## Changelog
See `CHANGELOG.md`.

---
Minimal, experimental API—pin versions in production.