# Synchronous (State-backed) handler for AtomicState effect.
#
# Maps AtomicState operations to env.state storage, providing a simpler
# implementation for testing without spinning up Agent processes. Operations
# are "atomic" trivially since there's no concurrency in single-process tests.
#
# ## Usage
#
# comp do
# _ <- AtomicState.modify(&(&1 + 1))
# AtomicState.get()
# end
# |> AtomicState.Sync.with_handler(0)
# |> Comp.run!()
#
# ## Multiple Tagged States
#
# comp do
# _ <- AtomicState.modify(:a, &(&1 + 1))
# _ <- AtomicState.modify(:b, &(&1 * 2))
# {AtomicState.get(:a), AtomicState.get(:b)}
# end
# |> AtomicState.Sync.with_handler(0, tag: :a)
# |> AtomicState.Sync.with_handler(10, tag: :b)
# |> Comp.run!()
defmodule Skuld.Effects.AtomicState.Sync do
@moduledoc false
@behaviour Skuld.Comp.IHandle
@behaviour Skuld.Comp.IInstall
alias Skuld.Comp
alias Skuld.Comp.Env
alias Skuld.Comp.Types
alias Skuld.Effects.AtomicState
@default_tag AtomicState
# Op atoms from parent effect
@get_op AtomicState.get_op()
@put_op AtomicState.put_op()
@modify_op AtomicState.modify_op()
@atomic_state_op AtomicState.atomic_state_op()
@cas_op AtomicState.cas_op()
#############################################################################
## Handler Installation
#############################################################################
@doc """
Install a State-backed AtomicState handler.
## Options
- `tag` - the state tag (default: `Skuld.Effects.AtomicState`)
- `output` - optional output transform function
- `suspend` - optional suspend transform function
"""
@spec with_handler(Types.computation(), term(), keyword()) :: Types.computation()
def with_handler(comp, initial, opts \\ []) do
tag = Keyword.get(opts, :tag, @default_tag)
output = Keyword.get(opts, :output)
suspend = Keyword.get(opts, :suspend)
sk = AtomicState.state_key(tag)
handler_sig = AtomicState.sig(tag)
scoped_opts =
[]
|> then(fn o -> if output, do: Keyword.put(o, :output, output), else: o end)
|> then(fn o -> if suspend, do: Keyword.put(o, :suspend, suspend), else: o end)
# Op atoms captured for handler closure
get_op = @get_op
put_op = @put_op
modify_op = @modify_op
atomic_state_op = @atomic_state_op
cas_op = @cas_op
handler = fn args, env, k ->
case args do
^get_op ->
value = Env.get_state!(env, sk)
k.(value, env)
{^put_op, value} ->
new_env = Env.put_state(env, sk, value)
k.(:ok, new_env)
{^modify_op, fun} ->
current = Env.get_state!(env, sk)
new_value = fun.(current)
new_env = Env.put_state(env, sk, new_value)
k.(new_value, new_env)
{^atomic_state_op, fun} ->
current = Env.get_state!(env, sk)
{result, new_state} = fun.(current)
new_env = Env.put_state(env, sk, new_state)
k.(result, new_env)
{^cas_op, expected, new} ->
current = Env.get_state!(env, sk)
if current == expected do
new_env = Env.put_state(env, sk, new)
k.(:ok, new_env)
else
k.({:conflict, current}, env)
end
end
end
comp
|> Comp.with_scoped_state(sk, initial, scoped_opts)
|> Comp.with_handler(handler_sig, handler)
end
#############################################################################
## IInstall Implementation
#############################################################################
@doc """
Install handler via catch clause syntax.
catch
AtomicState.Sync -> 0
AtomicState.Sync -> {0, tag: :counter}
"""
@impl Skuld.Comp.IInstall
def __handle__(comp, {initial, opts}) when is_list(opts), do: with_handler(comp, initial, opts)
def __handle__(comp, initial), do: with_handler(comp, initial)
#############################################################################
## IHandle Implementation (default-tag fallback)
#############################################################################
@impl Skuld.Comp.IHandle
def handle(op, env, k) do
sk = AtomicState.state_key(@default_tag)
case op do
@get_op ->
value = Env.get_state!(env, sk)
k.(value, env)
{@put_op, value} ->
new_env = Env.put_state(env, sk, value)
k.(:ok, new_env)
{@modify_op, fun} ->
current = Env.get_state!(env, sk)
new_value = fun.(current)
new_env = Env.put_state(env, sk, new_value)
k.(new_value, new_env)
{@atomic_state_op, fun} ->
current = Env.get_state!(env, sk)
{result, new_state} = fun.(current)
new_env = Env.put_state(env, sk, new_state)
k.(result, new_env)
{@cas_op, expected, new} ->
current = Env.get_state!(env, sk)
if current == expected do
new_env = Env.put_state(env, sk, new)
k.(:ok, new_env)
else
k.({:conflict, current}, env)
end
end
end
end