Skuld.Effects.AtomicState (skuld v0.23.0)

View Source

AtomicState effect - thread-safe state for concurrent contexts.

Unlike the regular State effect which stores state in env.state (copied when forking to new processes), AtomicState uses external storage (Agent) that can be safely accessed from multiple processes.

Supports both simple single-state usage and multiple independent states via tags.

Handlers

  • AtomicState.Agent - Agent-backed handler for production (true atomic ops)
  • AtomicState.Sync - State-backed handler for testing (no Agent processes)

Production Usage (Agent handler)

use Skuld.Syntax
alias Skuld.Effects.AtomicState

comp do
  _ <- AtomicState.put(0)
  _ <- AtomicState.modify(&(&1 + 1))
  value <- AtomicState.get()
  value
end
|> AtomicState.Agent.with_handler(0)
|> Comp.run!()
#=> 1

Multiple States (explicit tags)

comp do
  _ <- AtomicState.put(:counter, 0)
  _ <- AtomicState.modify(:counter, &(&1 + 1))
  count <- AtomicState.get(:counter)

  _ <- AtomicState.put(:cache, %{})
  _ <- AtomicState.modify(:cache, &Map.put(&1, :key, "value"))
  cache <- AtomicState.get(:cache)

  {count, cache}
end
|> AtomicState.Agent.with_handler(0, tag: :counter)
|> AtomicState.Agent.with_handler(%{}, tag: :cache)
|> Comp.run!()
#=> {1, %{key: "value"}}

Compare-and-Swap (CAS)

comp do
  _ <- AtomicState.put(10)
  result1 <- AtomicState.cas(10, 20)  # succeeds
  result2 <- AtomicState.cas(10, 30)  # fails - current is 20, not 10
  {result1, result2}
end
|> AtomicState.Agent.with_handler(0)
|> Comp.run!()
#=> {:ok, {:conflict, 20}}

Testing (Sync handler)

For testing without spinning up Agents, use the Sync handler:

comp do
  _ <- AtomicState.put(0)
  _ <- AtomicState.modify(&(&1 + 1))
  AtomicState.get()
end
|> AtomicState.Sync.with_handler(0)
|> Comp.run!()
#=> 1

Per-tag dispatch

Each tag gets its own handler sig (a module atom) and compact operation tuples. The tag is encoded in the sig, not in the operation args:

  • getComp.effect(sig(tag), AtomicState.Get) — bare atom
  • put(v)Comp.effect(sig(tag), {AtomicState.Put, v}) — 2-tuple
  • modify(f)Comp.effect(sig(tag), {AtomicState.Modify, f})
  • atomic_state(f)Comp.effect(sig(tag), {AtomicState.AtomicState, f})
  • cas(e, n)Comp.effect(sig(tag), {AtomicState.Cas, e, n}) — 3-tuple

Summary

Functions

Install AtomicState handler via catch clause syntax.

Returns the env.state key used for storing the Agent pid for a given tag.

Returns the env.state key used for State-backed storage for a given tag.

Install an Agent-backed AtomicState handler.

Install a State-backed AtomicState handler for testing.

Functions

__handle__(comp, arg)

Install AtomicState handler via catch clause syntax.

Config selects handler type:

catch
  AtomicState -> {:agent, 0}                    # agent handler
  AtomicState -> {:agent, {0, tag: :counter}}   # agent with opts
  AtomicState -> {:sync, 0}                     # sync handler
  AtomicState -> {:sync, {0, tag: :counter}}    # sync with opts

agent_key(tag)

@spec agent_key(atom()) :: atom()

Returns the env.state key used for storing the Agent pid for a given tag.

state_key(tag)

@spec state_key(atom()) :: atom()

Returns the env.state key used for State-backed storage for a given tag.

with_agent_handler(comp, initial, opts \\ [])

Install an Agent-backed AtomicState handler.

Delegates to AtomicState.Agent.with_handler/3.

with_state_handler(comp, initial, opts \\ [])

Install a State-backed AtomicState handler for testing.

Delegates to AtomicState.Sync.with_handler/3.