Packages
finitomata
0.41.0
0.41.0
0.40.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.30.3
0.30.2
0.30.1
0.30.0
0.29.10
0.29.9
0.29.8
0.29.7
0.29.6
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.4
0.26.3
0.26.2
0.26.1
0.26.0
0.25.0
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.7
0.23.6
0.23.5
0.23.4
0.23.3
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.1
0.15.0
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.1
0.1.0
The FSM implementation generated from PlantUML textual representation.
Current section
Files
Jump to
Current section
Files
lib/finitomata/persistency/snapshot.ex
defmodule Finitomata.Persistency.Snapshot do
@moduledoc false
# Shared implementation backing the built-in `Finitomata.Persistency.ETS` and
# `Finitomata.Persistency.DETS` adapters.
#
# Both adapters keep a single `{state, payload}` snapshot per FSM, keyed by the FSM
# name, plus an optional last-error record. They differ only in the storage `kind`
# (`:ets` for in-memory, `:dets` for disk-backed); the `:ets`/`:dets` read/write
# primitives share the same `{key, value}` shape, so the orchestration lives here
# once and each adapter passes its own `kind` and resolved table name.
#
# Key derivation follows the contract `Finitomata.Engine` actually uses:
# * `store/store_error` receive the fully-qualified FSM `name`, normalized with
# `idfy/1` (the registered plain name);
# * `load` receives the `{type, fields}` tuple the engine builds from the start
# payload, and the storage key is taken from `fields[:id]`.
# These coincide whenever the FSM name equals the entity id, which is the
# documented usage pattern (see `Finitomata.Persistency`).
alias Finitomata.State
@error_tag :__finitomata_error__
@typedoc "The storage kind backing an adapter"
@type kind :: :ets | :dets
@doc "Normalizes a (possibly registered) FSM name into the bare storage key"
@spec idfy(Finitomata.fsm_name()) :: term()
def idfy({:via, _registry, {_registry_name, name}}), do: name
def idfy(name), do: name
@doc """
Loads the snapshot for the entity described by the engine's `{type, fields}` tuple.
Returns `{:loaded, {state, payload}}` when a snapshot exists, otherwise
`{:created, {nil, fresh_payload}}` where `fresh_payload` is reconstructed from the
start payload. The first snapshot is written by the entry transition.
"""
@spec load(kind(), atom(), term()) :: {:loaded | :created, Finitomata.Persistency.snapshot()}
def load(kind, table, {_type, _fields} = arg) do
case safe_lookup(kind, table, load_key(arg)) do
[{_key, {_state, _payload} = snapshot}] -> {:loaded, snapshot}
_ -> {:created, {nil, fresh(arg)}}
end
end
def load(kind, table, id) do
case safe_lookup(kind, table, id) do
[{_key, {_state, _payload} = snapshot}] -> {:loaded, snapshot}
_ -> {:created, {nil, %{}}}
end
end
@doc "Persists the post-transition `{to, payload}` snapshot keyed by the FSM name"
@spec store(kind(), atom(), Finitomata.fsm_name(), State.payload(), map()) :: :ok
def store(kind, table, name, payload, %{to: to}) do
put(kind, table, {idfy(name), {to, payload}})
:ok
end
@doc "Persists a transition failure keyed by the FSM name; returns `:ok`"
@spec store_error(kind(), atom(), Finitomata.fsm_name(), State.payload(), any(), map()) :: :ok
def store_error(kind, table, name, payload, reason, info) do
put(kind, table, {{@error_tag, idfy(name)}, %{reason: reason, info: info, payload: payload}})
:ok
end
@doc "Reads the snapshot stored for `name`, or `nil`"
@spec get(kind(), atom(), Finitomata.fsm_name()) :: Finitomata.Persistency.snapshot() | nil
def get(kind, table, name) do
case safe_lookup(kind, table, idfy(name)) do
[{_key, {_state, _payload} = snapshot}] -> snapshot
_ -> nil
end
end
@doc "Reads the last persisted failure for `name`, or `nil`"
@spec last_error(kind(), atom(), Finitomata.fsm_name()) :: Finitomata.Persistency.error() | nil
def last_error(kind, table, name) do
case safe_lookup(kind, table, {@error_tag, idfy(name)}) do
[{_key, %{} = error}] -> error
_ -> nil
end
end
@doc "Removes the snapshot and the last error stored for `name`"
@spec purge(kind(), atom(), Finitomata.fsm_name()) :: :ok
def purge(kind, table, name) do
key = idfy(name)
delete(kind, table, key)
delete(kind, table, {@error_tag, key})
:ok
end
# The engine injects the FSM `name` (the fully-qualified via-tuple) as `fields[:id]`
# when the start payload carries no id of its own, while `store/store_error` key by
# `idfy(name)`. Normalizing here keeps both sides on the same plain key.
@spec load_key(term()) :: term()
defp load_key({_type, fields}), do: fields |> to_map() |> Map.get(:id) |> idfy()
@spec fresh(term()) :: State.payload()
defp fresh({type, fields}) do
map = to_map(fields)
if struct_module?(type),
do: struct(type, Map.delete(map, :id)),
else: Map.delete(map, :id)
end
@spec to_map(term()) :: map()
defp to_map(fields) when is_list(fields), do: Map.new(fields)
defp to_map(%{} = fields), do: fields
defp to_map(_other), do: %{}
@spec struct_module?(term()) :: boolean()
defp struct_module?(type) when is_atom(type),
do: Code.ensure_loaded?(type) and function_exported?(type, :__struct__, 0)
defp struct_module?(_other), do: false
@spec safe_lookup(kind(), atom(), term()) :: [tuple()]
defp safe_lookup(:ets, table, key) do
:ets.lookup(table, key)
rescue
ArgumentError -> []
end
defp safe_lookup(:dets, table, key) do
case :dets.lookup(table, key) do
list when is_list(list) -> list
_other -> []
end
rescue
_error -> []
end
@spec put(kind(), atom(), tuple()) :: :ok
defp put(:ets, table, object) do
if :ets.whereis(table) != :undefined, do: :ets.insert(table, object)
:ok
end
defp put(:dets, table, object) do
_ = :dets.insert(table, object)
:ok
rescue
_error -> :ok
end
@spec delete(kind(), atom(), term()) :: :ok
defp delete(:ets, table, key) do
if :ets.whereis(table) != :undefined, do: :ets.delete(table, key)
:ok
end
defp delete(:dets, table, key) do
_ = :dets.delete(table, key)
:ok
rescue
_error -> :ok
end
end