Current section
Files
Jump to
Current section
Files
lib/live_stash.ex
defmodule LiveStash do
@moduledoc """
Main public API for stashing and recovering `Phoenix.LiveView` assigns.
`LiveStash` helps preserve selected server-side assigns across reconnects.
You explicitly choose which assigns to persist and when to persist them.
This module:
- integrates with `on_mount` via `use LiveStash`
- initializes the selected adapter
- delegates persistence and recovery operations to that adapter
## Quick start
Add `use LiveStash` to your LiveView:
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
use LiveStash, stored_keys: [:count]
end
Stash assigns after state-changing events:
def handle_event("increment", _, socket) do
socket
|> assign(:count, socket.assigns.count + 1)
|> LiveStash.stash()
|> then(&{:noreply, &1})
end
Recover stashed state in `mount/3`:
def mount(_params, _session, socket) do
socket
|> LiveStash.recover_state()
|> case do
{:recovered, recovered_socket} ->
# socket with previously stashed assigns is recovered
recovered_socket
{_, socket} ->
# could not recover assigns, proceed with standard setup using returned socket
# ...
end
|> then(&{:ok, &1})
end
## Recovery statuses
`recover_state/1` returns `{status, socket}` where `status` is one of:
- `:new` - fresh LiveView process, with no previously stashed state
- `:recovered` - state was found and applied
- `:not_found` - state was not found for this LiveView
- `:error` - adapter failed to recover state
## Adapter selection
The default adapter is `LiveStash.Adapters.BrowserMemory`.
You can override it per LiveView:
use LiveStash, adapter: LiveStash.Adapters.ETS
Adapters used by your app must also be enabled in config:
config :live_stash,
adapters: [LiveStash.Adapters.BrowserMemory, LiveStash.Adapters.ETS]
"""
alias Phoenix.LiveView.Socket
alias Phoenix.LiveView
alias LiveStash.Utils
alias LiveStash.OptsHelpers
require Logger
@type recovery_status :: :recovered | :not_found | :new | :error
@doc """
Injects LiveStash support into a `Phoenix.LiveView`. This macro expands to:
on_mount({LiveStash, opts})
so that LiveStash can initialize stash handling during the LiveView `mount/3`
lifecycle.
## Options
The `opts` are forwarded to `LiveStash.on_mount/4` and ultimately to the
configured adapter. Most adapters use `:adapter` to select the persistence
backend:
use LiveStash, adapter: LiveStash.Adapters.ETS
Note: adapters must also be enabled in `config :live_stash, :adapters`.
## Example
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
use LiveStash, adapter: LiveStash.Adapters.BrowserMemory, security_mode: :sign, stored_keys: [:count]
end
"""
defmacro __using__(opts) do
quote do
on_mount({LiveStash, unquote(opts)})
end
end
@doc """
LiveView `on_mount` callback used by `use LiveStash`.
It initializes stash handling for the current socket and continues the mount
lifecycle.
"""
def on_mount(_opts, :not_mounted_at_router, _session, _socket) do
raise ArgumentError, "LiveStash does not support nested LiveViews."
end
def on_mount(opts, _params, session, socket) do
socket = init_stash(socket, session, opts)
{:cont, socket}
end
@doc """
Initializes stash support for a socket using the configured adapter.
This function is called from `on_mount/4`. In normal usage, prefer
`use LiveStash` and do not call this function directly.
It validates that the selected adapter is active in
`config :live_stash, :adapters`.
"""
@spec init_stash(socket :: Socket.t(), session :: Keyword.t(), opts :: Keyword.t()) ::
Socket.t()
def init_stash(socket, session, opts \\ []) do
{adapter, opts} = Keyword.pop(opts, :adapter, LiveStash.Adapter.default())
OptsHelpers.ensure_stored_keys!(opts)
OptsHelpers.ensure_adapter_active!(adapter)
socket
|> OptsHelpers.handle_auto_stash(opts)
|> LiveView.put_private(:live_stash_adapter, adapter)
|> adapter.init_stash(session, opts)
end
@doc """
Stashes assigns from `socket.assigns` declared at the module level.
## Examples
use LiveStash, stored_keys: [:count, :username] # assigns are declared at the module level
def handle_event("increment", _, socket) do
socket
|> assign(:count, socket.assigns.count + 1)
|> LiveStash.stash()
|> then(&{:noreply, &1})
end
"""
@spec stash(socket :: Socket.t()) :: Socket.t()
def stash(socket) do
socket
|> get_adapter()
|> apply(:stash, [socket])
end
@doc """
Recovers previously stashed state and returns `{status, socket}`.
This function is typically called in `mount/3`. Recovery does not clear the
stored state; use `reset_stash/1` when you want to remove it explicitly.
## Examples
def mount(_params, _session, socket) do
socket
|> LiveStash.recover_state()
|> case do
{:recovered, recovered_socket} ->
recovered_socket
{_, socket} ->
start_new_game(socket)
end
|> then(&{:ok, &1})
end
"""
@spec recover_state(socket :: Socket.t()) :: {recovery_status(), Socket.t()}
def recover_state(socket) do
socket
|> get_adapter()
|> apply(:recover_state, [socket])
end
@doc """
Clears stashed state for the current LiveView socket.
## Examples
def handle_event("restart_game", _params, socket) do
socket
|> LiveStash.reset_stash()
|> start_new_game()
|> then(&{:noreply, &1})
end
"""
@spec reset_stash(socket :: Socket.t()) :: Socket.t()
def reset_stash(socket) do
socket
|> get_adapter()
|> apply(:reset_stash, [socket])
end
defp get_adapter(%{private: %{live_stash_adapter: adapter}}), do: adapter
defp get_adapter(_) do
msg =
Utils.reason_message(
"LiveStash has not been initialized, please use on_mount/1 to initialize it",
:error
)
raise ArgumentError, msg
end
end