# A serializable snapshot of `env.state` for cold resume.
#
# All env.state keys are strings, so they pass through JSON natively
# without needing encode/decode conversion.
#
# ## Filtering
#
# EffectLogger's internal state keys are filtered out when capturing:
# - Log state key: would create circular reference (log contains snapshot)
# - Resume value key: only needed during active resume, not for persistence
# - State keys filter: internal config that doesn't survive JSON round-trip
#
# ## Example
#
# env_state = %{
# "Elixir.Skuld.Effects.State" => 42,
# "Elixir.Skuld.Effects.EffectLogger::log" => %Log{...}
# }
#
# snapshot = EnvStateSnapshot.capture(env_state)
# # => %EnvStateSnapshot{entries: %{"Elixir.Skuld.Effects.State" => 42}}
#
# restored = EnvStateSnapshot.restore(snapshot)
# # => %{"Elixir.Skuld.Effects.State" => 42}
defmodule Skuld.Effects.EffectLogger.EnvStateSnapshot do
@moduledoc false
alias Skuld.Comp.SerializableStruct
@effect_logger_state_key "Elixir.Skuld.Effects.EffectLogger::log"
@resume_value_key "Elixir.Skuld.Effects.EffectLogger::resume_value"
@state_keys_key "Elixir.Skuld.Effects.EffectLogger::state_keys"
defstruct entries: %{}
@type t :: %__MODULE__{
entries: %{String.t() => term()}
}
@doc """
Capture a snapshot of env.state for serialization.
Filters out EffectLogger's internal state. All keys are already
strings so no conversion is needed.
## Options
- `:state_keys` - List of state keys to include. Default `:all` captures everything.
Keys should be in the format used in env.state (strings).
## Examples
# Capture all state
EnvStateSnapshot.capture(env_state)
# Capture only specific State effect keys
EnvStateSnapshot.capture(env_state, state_keys: [
State.state_key(MyApp.Counter)
])
"""
@spec capture(map(), keyword()) :: t()
def capture(env_state, opts \\ [])
def capture(env_state, opts) when is_map(env_state) do
state_keys = Keyword.get(opts, :state_keys, :all)
entries =
env_state
|> Enum.reject(fn {key, _value} ->
key == @effect_logger_state_key or key == @resume_value_key or key == @state_keys_key
end)
|> Enum.filter(fn {key, _value} ->
case state_keys do
:all -> true
nil -> true
keys when is_list(keys) -> key in keys
end
end)
|> Enum.map(fn {key, value} ->
{key, encode_value(value)}
end)
|> Map.new()
%__MODULE__{entries: entries}
end
@doc """
Restore env.state from a snapshot.
Keys are already strings, so they pass through unchanged.
"""
@spec restore(t()) :: map()
def restore(%__MODULE__{entries: entries}) do
entries
|> Enum.map(fn {key, value} ->
{key, decode_value(value)}
end)
|> Map.new()
end
@doc """
Reconstruct from decoded JSON map.
"""
@spec from_json(map()) :: t()
def from_json(map) when is_map(map) do
entries =
(map["entries"] || %{})
|> Enum.map(fn {k, v} ->
{k, decode_value(v)}
end)
|> Map.new()
%__MODULE__{entries: entries}
end
# Encode a value for JSON serialization
defp encode_value(%_{} = struct) do
SerializableStruct.encode(struct)
end
defp encode_value(value), do: value
# Decode a value from JSON
defp decode_value(map) when is_map(map) do
if Map.has_key?(map, "__struct__") or Map.has_key?(map, :__struct__) do
SerializableStruct.decode(map)
else
map
end
end
defp decode_value(value), do: value
end
defimpl Jason.Encoder, for: Skuld.Effects.EffectLogger.EnvStateSnapshot do
alias Skuld.Comp.SerializableStruct
def encode(value, opts) do
value
|> SerializableStruct.encode()
|> Jason.Encode.map(opts)
end
end