eparch/state_machine

A type-safe, OTP-compatible, finite state machine implementation that leverages Erlang’s gen_statem behavior through the Gleam ffi.

Differences from gen_statem

Unlike Erlang’s gen_statem, this implementation:

Types

Actions (side effects) to perform after handling an event.

Multiple actions can be returned as a list.

pub type Action(message, reply) {
  Reply(from: From(reply), response: reply)
  Postpone
  NextEvent(event_type: NextEventType(reply), content: message)
  StateTimeout(time: TimeoutTime, content: message)
  EventTimeout(time: TimeoutTime, content: message)
  GenericTimeout(
    name: String,
    time: TimeoutTime,
    content: message,
  )
  Hibernate
  CancelStateTimeout
  CancelEventTimeout
  CancelGenericTimeout(name: String)
  UpdateStateTimeout(content: message)
  UpdateEventTimeout(content: message)
  UpdateGenericTimeout(name: String, content: message)
  ChangeCallbackModule(module: atom.Atom)
  PushCallbackModule(module: atom.Atom)
  PopCallbackModule
}

Constructors

  • Reply(from: From(reply), response: reply)

    Send a reply to a caller

  • Postpone

    Postpone this event until after a state change

  • NextEvent(event_type: NextEventType(reply), content: message)

    Insert a new event at the front of the queue Inject a synthetic event of any kind that gen_statem accepts. Mirrors gen_statem:event_type/0.

  • StateTimeout(time: TimeoutTime, content: message)

    Set a state timeout (canceled on state change). content is delivered as the Timeout event payload when the timer fires.

  • EventTimeout(time: TimeoutTime, content: message)

    Set an event timeout, the unnamed timeout() action from gen_statem. Automatically canceled by any other event arriving in this state, so the timer effectively measures inactivity since the last event.

  • GenericTimeout(name: String, time: TimeoutTime, content: message)

    Set a named generic timeout. Persists across state changes and is addressable by name for cancel/update.

  • Hibernate

    Hibernate the process after this callback returns.

  • CancelStateTimeout

    Cancel the running state timeout before it fires. Since OTP 22.1.

  • CancelEventTimeout

    Cancel the running event timeout before it fires. Since OTP 22.1.

  • CancelGenericTimeout(name: String)

    Cancel a running named generic timeout before it fires. Since OTP 22.1.

  • UpdateStateTimeout(content: message)

    Update the payload delivered when the state timeout fires, without restarting the timer. Since OTP 22.1.

  • UpdateEventTimeout(content: message)

    Update the payload delivered when the event timeout fires, without restarting the timer. Since OTP 22.1.

  • UpdateGenericTimeout(name: String, content: message)

    Update the payload delivered when a named generic timeout fires, without restarting the timer. Since OTP 22.1.

  • ChangeCallbackModule(module: atom.Atom)

    Change the gen_statem callback module to module. The new module receives the internal #gleam_statem record as its data, use only for Erlang interop with modules that understand eparch’s internals.

  • PushCallbackModule(module: atom.Atom)

    Push the current callback module onto an internal stack and switch to module. Pop with PopCallbackModule to restore. Otherwise like ChangeCallbackModule.

  • PopCallbackModule

    Pop the top module from the internal callback-module stack and switch to it. Fails the server if the stack is empty.

An armed (not-yet-fired) timeout on the state machine.

ActiveOtherTimeout handles any shape the FFI cannot classify as a state, event, or generic timeout.

pub type ActiveTimeout(message) {
  ActiveStateTimeout(content: message)
  ActiveEventTimeout(content: message)
  ActiveGenericTimeout(name: String, content: message)
  ActiveOtherTimeout(raw: dynamic.Dynamic)
}

Constructors

  • ActiveStateTimeout(content: message)
  • ActiveEventTimeout(content: message)
  • ActiveGenericTimeout(name: String, content: message)
  • ActiveOtherTimeout(raw: dynamic.Dynamic)

A builder for configuring a state machine before starting it.

Generic parameters:

  • state: The type of state values (e.g., enum, custom type)
  • data: The type of data carried across state transitions
  • message: The type of messages the state machine receives
  • reply: The type of replies produced for synchronous Call events
pub opaque type Builder(state, data, message, reply)

The result of waiting for a response from a RequestIdCollection.

pub type CollectionResponse(reply, label) {
  GotReply(
    reply: reply,
    label: label,
    remaining: RequestIdCollection(label, reply),
  )
  RequestFailed(
    reason: StopReason,
    label: label,
    remaining: RequestIdCollection(label, reply),
  )
  NoRequests
}

Constructors

  • GotReply(
      reply: reply,
      label: label,
      remaining: RequestIdCollection(label, reply),
    )

    A successful reply was received for one of the pending requests.

  • RequestFailed(
      reason: StopReason,
      label: label,
      remaining: RequestIdCollection(label, reply),
    )

    One of the requests returned an error (e.g. the server crashed).

  • NoRequests

    The collection had no pending requests.

Events that a state machine can receive.

This unifies the three types of messages in OTP:

  • Calls (synchronous, requires reply)
  • Casts (asynchronous / fire-and-forget)
  • Info (other messages, from selectors/monitors)
pub type Event(state, message, reply) {
  Call(from: From(reply), message: message)
  Cast(message: message)
  Info(message: message)
  Enter(old_state: state)
  Timeout(timeout: TimeoutType, content: message)
}

Constructors

  • Call(from: From(reply), message: message)

    A synchronous call that expects a reply

  • Cast(message: message)

    An asynchronous cast (fire-and-forget)

  • Info(message: message)

    An info message (from selectors, monitors, etc)

  • Enter(old_state: state)

    Internal event fired when entering a state (if state_enter enabled) Contains the previous state

  • Timeout(timeout: TimeoutType, content: message)

    Timeout events (state, event, or generic timeout).

    content is the payload supplied when the timer was armed (or via UpdateStateTimeout / UpdateEventTimeout / UpdateGenericTimeout).

Opaque reference to a caller (for replying to calls).

Represents Erlang’s gen_statem:from() type. Values of this type only ever originate from a Call event delivered by the gen_statem runtime.

pub type From(reply)

Data returned by start_monitor: a started machine plus the OTP-23+ atomic monitor for it.

pub type MonitoredMachine(message) {
  MonitoredMachine(
    pid: process.Pid,
    ref: ServerRef(message),
    monitor: process.Monitor,
  )
}

Constructors

Mirrors gen_statem’s event_type/0 so a single NextEvent can inject any kind of synthetic event into the state machine. https://www.erlang.org/doc/apps/stdlib/gen_statem.html#t:event_type/0

pub type NextEventType(reply) {
  InternalEvent
  CastEvent
  InfoEvent
  CallEvent(from: From(reply))
}

Constructors

  • InternalEvent

    Delivered as an internal event

  • CastEvent

    Delivered as a cast event

  • InfoEvent

    Delivered as an info event

  • CallEvent(from: From(reply))

    Delivered as a call event from the given caller

A pending event on the gen_statem queue or in the postponed list.

Mirrors the Event type but without the Enter variant (state-entry events never queue). QueuedOther is a fallback for shapes the FFI does not recognise, so the formatter never crashes on an unexpected OTP event type.

pub type QueuedEvent(message, reply) {
  QueuedCall(from: From(reply), message: message)
  QueuedCast(message: message)
  QueuedInfo(message: message)
  QueuedInternal(message: message)
  QueuedStateTimeout(content: message)
  QueuedEventTimeout(content: message)
  QueuedGenericTimeout(name: String, content: message)
  QueuedOther(raw: dynamic.Dynamic)
}

Constructors

  • QueuedCall(from: From(reply), message: message)
  • QueuedCast(message: message)
  • QueuedInfo(message: message)
  • QueuedInternal(message: message)
  • QueuedStateTimeout(content: message)
  • QueuedEventTimeout(content: message)
  • QueuedGenericTimeout(name: String, content: message)
  • QueuedOther(raw: dynamic.Dynamic)

Reasons receive_response and receive_response_collection can fail.

pub type ReceiveError {
  ReceiveTimeout
  RequestCrashed(reason: StopReason)
}

Constructors

  • ReceiveTimeout

    No reply was received within the timeout.

  • RequestCrashed(reason: StopReason)

    The server handling the request crashed or went away.

An opaque request ID returned by send_request.

The phantom type reply tracks the expected response type at compile time. Requires Erlang/OTP 25.0 or later.

pub type RequestId(reply)

A collection of in-flight request IDs, each associated with a label.

Used with send_request_to_collection, request_ids_add, and receive_response_collection to manage multiple concurrent requests. Requires Erlang/OTP 25.0 or later.

pub type RequestIdCollection(label, reply)

Whether receive_response_collection removes the matched request from the collection after delivering the reply.

pub type ResponseHandling {
  Delete
  Keep
}

Constructors

  • Delete
  • Keep

A name under which a state machine can be registered.

Mirrors gen_statem:server_name/0:

  • Local(name) registers locally with erlang:register/2 and gives back a process.Name(message) you can turn into a Subject(message).
  • Global(name) registers across nodes via the global module.
  • Via(module, name) dispatches registration through any module implementing the via registry behaviour (e.g. gproc, syn).
pub type ServerName(message) {
  Local(name: process.Name(message))
  Global(name: atom.Atom)
  Via(module: atom.Atom, name: dynamic.Dynamic)
}

Constructors

An opaque, phantom-typed handle to a running state machine.

Returned by start_link, start, and start_monitor. Accepted by cast, send_request, and send_request_to_collection. For unnamed and Local-named servers, ref_to_subject exposes the underlying Subject(message) so that process.send, state_machine.send, and state_machine.call can be used as well.

pub type ServerRef(message)

Errors that can occur when starting a state machine.

pub type StartError {
  InitTimeout
  InitFailed(String)
  InitExited(process.ExitReason)
  AlreadyStarted(pid: process.Pid)
}

Constructors

  • InitTimeout
  • InitFailed(String)
  • InitExited(process.ExitReason)
  • AlreadyStarted(pid: process.Pid)

    The requested name was already registered to another process.

Convenience type for start results.

pub type StartResult(message) =
  Result(Started(message), StartError)

Data returned when a state machine starts successfully.

pub type Started(message) {
  Started(pid: process.Pid, ref: ServerRef(message))
}

Constructors

  • Started(pid: process.Pid, ref: ServerRef(message))

    Arguments

    pid

    The process identifier of the started state machine.

    ref

    Opaque handle suitable for cast, send_request, etc. For unnamed and Local-named starts, also convertible to a Subject(message) via ref_to_subject.

Snapshot of the state machine passed to the format_status callback.

Mirrors the OTP 25+ format_status/1 map for gen_statem. The state and data fields always carry the current typed values. The remaining fields correspond to OTP map keys that are only present in certain contexts (e.g. reason during termination, queue during sys calls). Return a modified Status from your formatter to control what sys:get_status/1 and SASL crash reports display.

log stays as List(Dynamic) because sys:system_event() is an internal Erlang shape with no stable Gleam equivalent.

pub type Status(state, data, message, reply) {
  Status(
    state: state,
    data: data,
    reason: option.Option(StopReason),
    queue: List(QueuedEvent(message, reply)),
    postponed: List(QueuedEvent(message, reply)),
    timeouts: List(ActiveTimeout(message)),
    log: List(dynamic.Dynamic),
  )
}

Constructors

The result of handling an event.

Indicates what the state machine should do next.

pub type Step(state, data, message, reply) {
  NextState(
    state: state,
    data: data,
    actions: List(Action(message, reply)),
  )
  KeepState(data: data, actions: List(Action(message, reply)))
  KeepStateAndData(actions: List(Action(message, reply)))
  RepeatState(data: data, actions: List(Action(message, reply)))
  RepeatStateAndData(actions: List(Action(message, reply)))
  Stop(reason: process.ExitReason)
  StopAndReply(
    reason: process.ExitReason,
    replies: List(Action(message, reply)),
  )
}

Constructors

  • NextState(
      state: state,
      data: data,
      actions: List(Action(message, reply)),
    )

    Transition to a new state

  • KeepState(data: data, actions: List(Action(message, reply)))

    Keep the current state, updating data

  • KeepStateAndData(actions: List(Action(message, reply)))

    Keep the current state without re-specifying data (OTP optimization).

  • RepeatState(data: data, actions: List(Action(message, reply)))

    Re-enter the current state with new data, re-triggering the state_enter callback if enabled.

  • RepeatStateAndData(actions: List(Action(message, reply)))

    Re-enter the current state without changing data.

  • Stop(reason: process.ExitReason)

    Stop the state machine

  • StopAndReply(
      reason: process.ExitReason,
      replies: List(Action(message, reply)),
    )

    Stop the state machine and atomically send replies to pending callers. Only Reply(from, response) actions are valid in the replies list.

Termination reason as seen by format_status/1.

Exit wraps a recognised process.ExitReason. RawReason is a fallback for reasons the FFI cannot classify (e.g. {shutdown, _}, {noproc, _}, arbitrary user terms) so callers can still inspect or replace them.

pub type StopReason {
  Exit(reason: process.ExitReason)
  RawReason(term: dynamic.Dynamic)
}

Constructors

When a timer should fire.

  • After(ms): relative delay from now, the common case.
  • At(ms): absolute erlang:monotonic_time(millisecond) deadline, maps to gen_statem’s [{abs, true}] timeout option.
pub type TimeoutTime {
  After(milliseconds: Int)
  At(monotonic_milliseconds: Int)
}

Constructors

  • After(milliseconds: Int)
  • At(monotonic_milliseconds: Int)

Types of timeouts.

Matches the three gen_statem timer classes:

  • StateTimeoutType: set with StateTimeout, canceled on state change.
  • EventTimeoutType: set with EventTimeout, canceled by any other event arriving in the current state (gen_statem’s unnamed timeout).
  • GenericTimeoutType(name): set with GenericTimeout, persists across state changes and is addressable by name.
pub type TimeoutType {
  StateTimeoutType
  EventTimeoutType
  GenericTimeoutType(name: String)
}

Constructors

  • StateTimeoutType
  • EventTimeoutType
  • GenericTimeoutType(name: String)

Values

pub fn call(
  subject: process.Subject(message),
  waiting timeout: Int,
  sending make_message: fn(process.Subject(reply)) -> message,
) -> reply

Send a synchronous call and wait for a reply.

This is a re-export of process.call for convenience.

pub fn cancel_event_timeout() -> Action(message, reply)

Cancel the running event timeout before it fires.

pub fn cancel_generic_timeout(
  name: String,
) -> Action(message, reply)

Cancel a running named generic timeout before it fires.

pub fn cancel_state_timeout() -> Action(message, reply)

Cancel the running state timeout before it fires.

pub fn cast(ref: ServerRef(message), message: message) -> Nil

Send an asynchronous cast to a state machine (arrives as Cast).

Unlike send, which routes messages through process.send and delivers them as Info(message), this function calls gen_statem:cast so messages arrive as Cast(message) in the event handler.

Use cast when you want to distinguish machine-level commands from ambient info messages (monitors, raw Erlang signals, etc.).

Example

fn handle_event(event, _state, data) {
  case event {
    Cast(Increment) -> keep_state(data + 1, [])
    Cast(_) -> keep_state(data, [])
    Info(_) -> keep_state(data, []) // ignore ambient noise
    Call(_, _) -> keep_state(data, [])
    Enter(_) -> keep_state(data, [])
    Timeout(_, _) -> keep_state(data, [])
  }
}
pub fn change_callback_module(
  module: atom.Atom,
) -> Action(message, reply)

Create a ChangeCallbackModule action.

Swaps the gen_statem callback module at runtime. The new module receives the internal #gleam_statem record as its data, so it must be an Erlang module written to understand eparch internals. Use only for advanced Erlang interop; most applications should not need this.

Since OTP 22.3.

Example

state_machine.change_callback_module(atom.create("my_erlang_module"))
pub fn check_response(
  message: dynamic.Dynamic,
  request_id: RequestId(reply),
) -> Result(option.Option(reply), ReceiveError)

Check whether a received message is the reply for a RequestId.

  • Ok(Some(reply)): the message is the reply
  • Ok(None): the message is unrelated to this request
  • Error(ReceiveError): the server crashed

Since OTP 23.

pub fn event_timeout(
  time: TimeoutTime,
  content: message,
) -> Action(message, reply)

Create an EventTimeout action: gen_statem’s unnamed timeout().

Canceled by any other event arriving in the current state.

pub fn generic_timeout(
  name: String,
  time: TimeoutTime,
  content: message,
) -> Action(message, reply)

Create a GenericTimeout action.

Sets a named timeout that persists across state changes.

pub fn hibernate() -> Action(message, reply)

Hibernate the process after this callback returns.

pub fn keep_state(
  data: data,
  actions: List(Action(message, reply)),
) -> Step(state, data, message, reply)

Create a KeepState step indicating no state change.

Example

keep_state(data, [])
pub fn keep_state_and_data(
  actions: List(Action(message, reply)),
) -> Step(state, data, message, reply)

Keep the current state without re-specifying data.

Use instead of keep_state when data has not changed, to avoid an unnecessary copy in the #gleam_statem record.

pub fn named(
  builder: Builder(state, data, message, reply),
  name: ServerName(message),
) -> Builder(state, data, message, reply)

Provide a name for the state machine to be registered with when started.

Pass Local(name) for the common local-atom registration (the only form compatible with a process.Subject), Global(name) for cluster-wide registration via the global module, or Via(module, term) for a custom registry such as gproc or syn.

pub fn new(
  initial_state initial_state: state,
  initial_data initial_data: data,
) -> Builder(state, data, message, reply)

Create a new state machine builder with initial state and data.

Defaults to a 1-second initialisation timeout, no idle-hibernation, no debug flags, no spawn options, and an unnamed (anonymous) registration.

Example

state_machine.new(initial_state: Idle, initial_data: 0)
|> state_machine.on_event(handle_event)
|> state_machine.start_link
pub fn next_event(
  event_type: NextEventType(reply),
  content: message,
) -> Action(message, reply)

Create a NextEvent action.

Inserts a new event at the front of the event queue.

pub fn next_state(
  state: state,
  data: data,
  actions: List(Action(message, reply)),
) -> Step(state, data, message, reply)

Create a NextState step indicating a state transition.

Example

next_state(Active, new_data, [state_timeout(After(5000), TimedOut)])
pub fn on_code_change(
  builder: Builder(state, data, message, reply),
  handler: fn(data) -> data,
) -> Builder(state, data, message, reply)

Provide a migration function called during hot-code upgrades.

When an OTP release upgrades the running code, gen_statem calls code_change/4. If a migration function is set, it receives the current data value and its return value becomes the new data. Use this to migrate data structures between versions without restarting the process.

If not set, the data passes through unchanged (the default and safe behaviour for most applications).

Example

// Old data shape: Int
// New data shape: Data(count: Int, label: String)
state_machine.new(Idle, 0)
|> state_machine.on_code_change(fn(old_count) { Data(old_count, "default") })
|> state_machine.on_event(handle_event)
|> state_machine.start_link
pub fn on_event(
  builder: Builder(state, data, message, reply),
  handler: fn(Event(state, message, reply), state, data) -> Step(
    state,
    data,
    message,
    reply,
  ),
) -> Builder(state, data, message, reply)

Set the event handler callback function.

This function is called for every event the state machine receives. It takes the current event, state, and data, and returns a Step indicating what to do next.

Example

fn handle_event(event, _state, data) {
  case event {
    Call(from, GetCount) -> reply_and_keep(from, data.count, data)
    Cast(Increment) -> keep_state(Data(..data, count: data.count + 1), [])
    Call(_, _) -> keep_state(data, [])
    Cast(_) -> keep_state(data, [])
    Info(_) -> keep_state(data, [])
    Enter(_) -> keep_state(data, [])
    Timeout(_, _) -> keep_state(data, [])
  }
}

state_machine.new(Running, Data(0))
|> state_machine.on_event(handle_event)
|> state_machine.start_link
pub fn on_format_status(
  builder: Builder(state, data, message, reply),
  formatter: fn(Status(state, data, message, reply)) -> Status(
    state,
    data,
    message,
    reply,
  ),
) -> Builder(state, data, message, reply)

Provide a formatter called when sys:get_status/1 or SASL crash reports render the state machine.

Maps to OTP’s format_status/1 gen_statem callback. The formatter receives a Status value containing the current state and data (plus optional fields described on the Status type) and returns a transformed Status. Use this to redact sensitive fields or produce a more readable representation.

If not set, sys:get_status/1 receives the raw internal state without transformation.

Example

state_machine.new(initial_state: Idle, initial_data: Credentials("secret"))
|> state_machine.on_format_status(fn(status) {
  Status(..status, data: Credentials("<redacted>"))
})
|> state_machine.on_event(handle_event)
|> state_machine.start_link
pub fn pop_callback_module() -> Action(message, reply)

Create a PopCallbackModule action.

Pops the top module off the callback-module stack and switches to it. Fails the server if the stack is empty, so only use after a matching push_callback_module.

Since OTP 22.3.

Example

state_machine.pop_callback_module()
pub fn postpone() -> Action(message, reply)

Create a Postpone action.

Postpones the current event until after the next state change.

pub fn push_callback_module(
  module: atom.Atom,
) -> Action(message, reply)

Create a PushCallbackModule action.

Pushes the current callback module onto an internal stack and switches to module. Restore the previous module with pop_callback_module. Same data-sharing caveats as change_callback_module apply.

Since OTP 22.3.

Example

state_machine.push_callback_module(atom.create("my_erlang_module"))
pub fn receive_response(
  request_id: RequestId(reply),
  timeout: Int,
) -> Result(reply, ReceiveError)

Wait up to timeout milliseconds for the reply to a single RequestId.

Returns Ok(reply) on success, Error(ReceiveTimeout) if no reply arrives in time, or Error(RequestCrashed(reason)) if the server terminated before replying.

Requires Erlang/OTP 25.0 or later.

pub fn receive_response_blocking(
  request_id: RequestId(reply),
) -> Result(reply, ReceiveError)

Block indefinitely until the reply to a RequestId arrives, using gen_statem:receive_response/1. Equivalent to receive_response with no timeout, complementing the timeout-bounded receive_response/2.

Requires Erlang/OTP 24 or later.

pub fn receive_response_collection(
  collection: RequestIdCollection(label, reply),
  timeout: Int,
  handling: ResponseHandling,
) -> CollectionResponse(reply, label)

Wait up to timeout milliseconds for any pending reply in a collection.

Pass Delete to remove the matched request from the returned collection, or Keep to retain it. Call this in a loop to drain all responses one by one.

Example

let assert state_machine.GotReply(value, label, collection) =
  state_machine.receive_response_collection(collection, 1000, state_machine.Delete)

Requires Erlang/OTP 25.0 or later.

pub fn ref_from_pid(pid: process.Pid) -> ServerRef(message)

Wrap a raw Pid as a ServerRef(message).

The phantom message parameter is unchecked; callers are responsible for passing the correct type. Use only when the Pid is known to belong to an eparch state machine handling messages of the expected type.

pub fn ref_from_subject(
  subject: process.Subject(message),
) -> ServerRef(message)

Wrap an existing Subject(message) as a ServerRef(message).

Useful when a state machine’s subject is already in hand (e.g. captured during start) and it needs to be passed to a function that expects a ServerRef.

pub fn ref_to_subject(
  ref: ServerRef(message),
) -> Result(process.Subject(message), Nil)

Extract the Subject(message) underlying a ServerRef(message).

Succeeds for refs returned by unnamed starts and Local-named starts. Returns Error(Nil) for refs registered through Global or Via, because those servers cannot be addressed by a Subject: they live outside the local-atom name table.

pub fn repeat_state(
  data: data,
  actions: List(Action(message, reply)),
) -> Step(state, data, message, reply)

Re-enter the current state with new data, re-triggering the state_enter callback if enabled.

pub fn repeat_state_and_data(
  actions: List(Action(message, reply)),
) -> Step(state, data, message, reply)

Re-enter the current state without changing data.

pub fn reply(
  from: From(reply),
  response: reply,
) -> Action(message, reply)

Create a Reply action.

Example

case event {
  Call(from, GetData) -> keep_state(data, [Reply(from, data)])
  _ -> keep_state(data, [])
}
pub fn reply_and_keep(
  from: From(reply),
  response: reply,
  data: data,
) -> Step(state, data, message, reply)

Reply and keep the current state.

Example

reply_and_keep(from, Ok(data.count), data)
pub fn reply_and_next(
  from: From(reply),
  response: reply,
  state: state,
  data: data,
) -> Step(state, data, message, reply)

Reply and transition to a new state.

Example

reply_and_next(from, Ok(Nil), Active, new_data)
pub fn request_ids_add(
  request_id request_id: RequestId(reply),
  label label: label,
  to collection: RequestIdCollection(label, reply),
) -> RequestIdCollection(label, reply)

Add a RequestId to a collection under a label.

The label is returned alongside the reply in receive_response_collection, letting you identify which request the response belongs to.

Requires Erlang/OTP 25.0 or later.

pub fn request_ids_new() -> RequestIdCollection(label, reply)

Create a new, empty request-id collection.

Used with send_request_to_collection to batch multiple async requests and then receive them through receive_response_collection.

Requires Erlang/OTP 25.0 or later.

pub fn request_ids_size(
  collection: RequestIdCollection(label, reply),
) -> Int

Return the number of pending request IDs in a collection.

Requires Erlang/OTP 25.0 or later.

pub fn request_ids_to_list(
  collection: RequestIdCollection(label, reply),
) -> List(#(RequestId(reply), label))

Convert a collection to a list of #(RequestId, label) pairs.

Requires Erlang/OTP 25.0 or later.

pub fn send(
  subject: process.Subject(message),
  message: message,
) -> Nil

Send a message to a state machine via process.send (arrives as Info).

The message is delivered to the handler as Info(message). Use this for messages sent from processes that are not aware of this library, e.g. monitors, timers, or plain Erlang processes.

To deliver messages as Cast(message) instead, use cast/2.

pub fn send_replies(replies: List(#(From(reply), reply))) -> Nil

Send multiple replies at once from outside the callback.

pub fn send_reply(from: From(reply), response: reply) -> Nil

Send a reply to a caller from outside the state machine callback.

Unlike the Reply action (which replies inside the callback), this can be called from any process that holds a From value.

pub fn send_request(
  ref: ServerRef(message),
  message: message,
) -> RequestId(reply)

Send an asynchronous call to a state machine and return a RequestId.

Unlike call, this does not block. Use receive_response later to collect the reply. The server receives a Call(from, message) event and must reply with a Reply(from, value) action.

The reply type cannot always be inferred, so annotate the binding when needed: let request: RequestId(MyReply) = send_request(machine.ref, MyMsg)

Example

let request: state_machine.RequestId(Int) =
  state_machine.send_request(machine.ref, GetCount)
// ... do other work ...
let assert Ok(count) = state_machine.receive_response(request, 1000)

Requires Erlang/OTP 25.0 or later.

pub fn send_request_to_collection(
  ref: ServerRef(message),
  message: message,
  label: label,
  to collection: RequestIdCollection(label, reply),
) -> RequestIdCollection(label, reply)

Send an asynchronous call and immediately add the RequestId to a collection.

Equivalent to calling send_request and request_ids_add in one step. Useful for issuing several requests in a loop before waiting for any of them.

Example

let collection: state_machine.RequestIdCollection(String, Int) =
  state_machine.request_ids_new()
let collection =
  state_machine.send_request_to_collection(sub, GetCount, "first", collection)
let collection =
  state_machine.send_request_to_collection(sub, GetCount, "second", collection)
// ... receive responses via receive_response_collection ...

Requires Erlang/OTP 25.0 or later.

pub fn start(
  builder: Builder(state, data, message, reply),
) -> Result(Started(message), StartError)

Start a state machine process without linking it to the caller.

Maps to gen_statem:start/3,4. Useful when the parent does not want a link-based crash propagation, e.g. when the parent will set up its own monitor or when the machine is started under a custom supervisor.

pub fn start_link(
  builder: Builder(state, data, message, reply),
) -> Result(Started(message), StartError)

Start a state machine process linked to the caller.

Maps to gen_statem:start_link/3,4. Returns a Started(message) value containing the PID and a ServerRef(message). For unnamed and Local- named starts, the ref is convertible to a Subject(message) via ref_to_subject.

Example

let assert Ok(machine) =
  state_machine.new(initial_state: Idle, initial_data: 0)
  |> state_machine.on_event(handle_event)
  |> state_machine.start_link

let assert Ok(subject) = state_machine.ref_to_subject(machine.ref)
process.send(subject, SomeMessage)
pub fn start_monitor(
  builder: Builder(state, data, message, reply),
) -> Result(MonitoredMachine(message), StartError)

Start a state machine process linked to the caller and atomically return a monitor for it.

Maps to gen_statem:start_monitor/3,4 (OTP 23.0+). Equivalent to start_link followed by process.monitor, but without the race window between the two calls.

pub fn state_timeout(
  time: TimeoutTime,
  content: message,
) -> Action(message, reply)

Create a StateTimeout action.

Sets a timeout that is automatically canceled when the state changes. content is delivered as the Timeout event payload when it fires.

pub fn stop(
  reason: process.ExitReason,
) -> Step(state, data, message, reply)

Create a Stop step indicating the state machine should terminate.

Example

stop(process.Normal)
pub fn stop_and_reply(
  reason: process.ExitReason,
  replies: List(Action(message, reply)),
) -> Step(state, data, message, reply)

Stop the state machine and atomically send replies to pending callers.

Only Reply(from, response) actions are valid in the replies list.

pub fn stop_server(subject: process.Subject(message)) -> Nil

Stop a running state machine from the client with reason normal.

pub fn stop_server_with(
  subject: process.Subject(message),
  reason: process.ExitReason,
  timeout: Int,
) -> Nil

Stop a running state machine from the client with a custom reason and timeout (ms).

pub fn update_event_timeout(
  content: message,
) -> Action(message, reply)

Update the payload of the running event timeout without restarting the timer.

pub fn update_generic_timeout(
  name: String,
  content: message,
) -> Action(message, reply)

Update the payload of a running named generic timeout without restarting the timer.

pub fn update_state_timeout(
  content: message,
) -> Action(message, reply)

Update the payload of the running state timeout without restarting the timer.

pub fn wait_response(
  request_id: RequestId(reply),
) -> Result(reply, ReceiveError)

Block indefinitely until a reply arrives for a RequestId.

Since OTP 23.

pub fn wait_response_timeout(
  request_id: RequestId(reply),
  timeout: Int,
) -> Result(reply, ReceiveError)

Block until a reply arrives or the timeout (ms) expires.

Since OTP 23.

pub fn with_debug(
  builder: Builder(state, data, message, reply),
  flags: List(start_options.DebugFlag),
) -> Builder(state, data, message, reply)

Set the sys debug flags forwarded to the state machine on start.

pub fn with_hibernate_after(
  builder: Builder(state, data, message, reply),
  timeout: start_options.Timeout,
) -> Builder(state, data, message, reply)

Configure the hibernate_after start option.

When the state machine has been idle for at least the given duration, the process hibernates (calls proc_lib:hibernate/3), trading a small wake-up cost for reduced memory footprint until the next event arrives. Useful for long-lived machines that spend most of their time waiting. Defaults to Infinity (never hibernate).

This is independent of the per-callback Hibernate action, which forces hibernation immediately after a single callback returns.

pub fn with_spawn_options(
  builder: Builder(state, data, message, reply),
  spawn_options: List(start_options.SpawnOption),
) -> Builder(state, data, message, reply)

Set the erlang:spawn_opt/2 options forwarded to the state machine process on start. link and monitor are not exposed here; they are determined by the lifecycle entry point used (start_link, start, start_monitor).

pub fn with_state_enter(
  builder: Builder(state, data, message, reply),
) -> Builder(state, data, message, reply)

Enable state_enter calls.

When enabled, your event handler will be called with an Enter event whenever the state changes. This allows you to perform actions when entering a state (like setting timeouts, logging, etc).

The Enter event contains the previous state.

Example

fn handle_event(event, _state, data) {
  case event {
    Enter(_old) -> keep_state(data, [state_timeout(After(30_000), Tick)])
    Call(_, _) -> keep_state(data, [])
    Cast(_) -> keep_state(data, [])
    Info(_) -> keep_state(data, [])
    Timeout(_, _) -> keep_state(data, [])
  }
}

state_machine.new(Idle, data)
|> state_machine.with_state_enter()
|> state_machine.on_event(handle_event)
|> state_machine.start_link
pub fn with_timeout(
  builder: Builder(state, data, message, reply),
  timeout: start_options.Timeout,
) -> Builder(state, data, message, reply)

Set the initialisation timeout. Forwarded to gen_statem as {timeout, _}. Defaults to Milliseconds(1000).

Search Document