Packages
mob
0.7.12
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.2
0.6.1
0.6.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.11
0.5.10
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
BEAM-on-device mobile framework for Elixir
Current section
Files
Jump to
Current section
Files
lib/mob/screen_state.ex
defmodule Mob.ScreenState do
@moduledoc """
Persistent store for screen assigns.
Backed by the app's configured Ecto Repo (via `config :mob, :repo, MyApp.Repo`).
All functions are no-ops when no Repo is configured, so screens compiled with
`vsn:` enabled don't crash in environments without a database.
## Setup
Generated projects include the required table migration and config automatically.
For existing projects, add to `config/config.exs`:
config :mob, :repo, MyApp.Repo
And run the migration (see `priv/repo/migrations/*_create_mob_screen_states.exs`
generated by `mix mob.new`).
## Storage
State is keyed by screen module name by default. Screens with per-user or
parameterised state should implement `screen_key/1`:
def screen_key(assigns), do: "\#{__MODULE__}:\#{assigns.user_id}"
## Serialisation
Values are encoded with `:erlang.term_to_binary/1` after stripping
non-serialisable terms (PIDs, references, ports, functions). The `:safe`
flag is used on decode to prevent atom-table pollution from untrusted data.
"""
@select_sql "SELECT vsn, data FROM mob_screen_states WHERE key = ?"
@upsert_sql """
INSERT OR REPLACE INTO mob_screen_states (key, vsn, data, updated_at)
VALUES (?, ?, ?, ?)
"""
@delete_sql "DELETE FROM mob_screen_states WHERE key = ?"
@doc """
Persist the current assigns of `socket` for `module`.
Calls `module.dump_state/1`, strips non-serialisable values, and writes to
the configured Repo. Returns `:ok` regardless — persistence failures are
silent so a missing or misconfigured Repo never crashes a screen.
"""
@spec dump(module(), Mob.Socket.t()) :: :ok
def dump(module, socket) do
with repo when not is_nil(repo) <- repo(),
key <- screen_key(module, socket),
vsn <- module.__mob_vsn__(),
raw <- module.dump_state(socket.assigns),
{:ok, data} <- safe_encode(raw) do
ts = System.system_time(:second)
apply(repo, :query!, [@upsert_sql, [key, vsn, data, ts]])
end
:ok
end
@doc """
Load previously persisted assigns for `module`.
Returns `{:ok, stored_vsn, raw_map}` when a record exists, `:not_found`
otherwise (including when no Repo is configured or the data cannot be decoded).
"""
@spec load(module(), Mob.Socket.t()) :: {:ok, non_neg_integer(), map()} | :not_found
def load(module, socket) do
with repo when not is_nil(repo) <- repo(),
key <- screen_key(module, socket) do
case apply(repo, :query!, [@select_sql, [key]]) do
%{rows: [[stored_vsn, blob]]} when is_binary(blob) ->
case safe_decode(blob) do
{:ok, raw} -> {:ok, stored_vsn, raw}
:error -> :not_found
end
_ ->
:not_found
end
else
_ -> :not_found
end
end
@doc """
Delete the persisted state for `module`. Used when resetting or logging out.
"""
@spec delete(module(), Mob.Socket.t()) :: :ok
def delete(module, socket) do
with repo when not is_nil(repo) <- repo(),
key <- screen_key(module, socket) do
apply(repo, :query!, [@delete_sql, [key]])
end
:ok
end
# ── Private ────────────────────────────────────────────────────────────────
defp repo, do: Application.get_env(:mob, :repo)
defp screen_key(module, socket) do
if function_exported?(module, :screen_key, 1),
do: module.screen_key(socket.assigns),
else: to_string(module)
end
defp safe_encode(term) do
{:ok, :erlang.term_to_binary(strip(term))}
rescue
_ -> :error
end
defp safe_decode(blob) do
{:ok, :erlang.binary_to_term(blob, [:safe])}
rescue
_ -> :error
end
defp strip(map) when is_map(map) and not is_struct(map) do
map
|> Enum.reject(fn {_k, v} -> ephemeral?(v) end)
|> Map.new(fn {k, v} -> {k, strip(v)} end)
end
defp strip(list) when is_list(list) do
list |> Enum.reject(&ephemeral?/1) |> Enum.map(&strip/1)
end
defp strip(v), do: v
defp ephemeral?(v),
do: is_pid(v) or is_reference(v) or is_port(v) or is_function(v)
end