Packages
Postgres-backed durable execution for Elixir: declare an FSM, the engine commits its state before each step proceeds, so instances survive process and node death and resume where they left off.
Current section
Files
Jump to
Current section
Files
lib/gen_durable/state.ex
defmodule GenDurable.State do
@moduledoc """
Typed FSM state: an Ecto embedded schema per FSM, encoded to/from jsonb.
Typically defined as a nested `State` module inside the FSM, where it is
adopted by convention (see `GenDurable.FSM`):
defmodule Checkout do
use GenDurable.FSM, version: 1
defmodule State do
use GenDurable.State
embedded_schema do
field :order, :integer
field :n, :integer, default: 0
end
end
end
The engine loads the jsonb column into the struct before `step/2`
(`from_db/2`) and dumps the returned struct back to jsonb on the outcome
(`to_db/2`). FSMs may also run with no state module, in which case state is a
plain string-keyed map (allowed, but unsupported — you are on your own).
"""
defmacro __using__(_opts) do
quote do
use Ecto.Schema
@primary_key false
end
end
@doc "Load a DB jsonb value (raw string or already-decoded map) into the FSM state."
def from_db(nil, data), do: decode(data)
def from_db(module, data), do: Ecto.embedded_load(module, decode(data), :json)
@doc "Dump an FSM state value returned by a step into a JSON string for a jsonb param."
def to_db(_module, %_{} = struct), do: Jason.encode!(Ecto.embedded_dump(struct, :json))
def to_db(_module, map) when is_map(map), do: Jason.encode!(map)
@doc """
Normalize user-supplied state (for `insert`) into a JSON string. Accepts the
state struct, or a map (atom or string keys) which is cast through the schema.
"""
def cast(nil, value) when is_map(value), do: Jason.encode!(value)
def cast(module, %_{} = struct), do: to_db(module, struct)
def cast(module, map) when is_map(map),
do: to_db(module, Ecto.embedded_load(module, json_normalize(map), :json))
defp decode(data) when is_binary(data), do: Jason.decode!(data)
defp decode(data) when is_map(data), do: data
defp decode(nil), do: %{}
defp json_normalize(map), do: Jason.decode!(Jason.encode!(map))
end