Module
pooka/otp/server
src/pooka/otp/server.pookaA typed gen_server.
A server is a process that keeps a piece of state and changes it one message at a time. You describe it with two functions:
initmakes the first state. It is given the server's own subject, so the server can send itself messages later, such as timer ticks.handletakes the state and one message, and says what happens next: carry on with a new state, stop, or fail.
import pooka/otp.{Subject}
import pooka/otp/server.{Next}
class Counter {}
class Add(amount: Int) is Counter
class Total(reply: Subject(Int)) is Counter
handle(total: Int, message: Counter) -> Next(Int) {
match message {
Add(amount) -> server.next(total + amount)
Total(reply) -> {
otp.send(reply, total)
server.next(total)
}
}
}
main() {
let assert Ok(counter) = server.start(server.spec(fn(_) { Ok(0) }, handle))
server.send(counter, Add(5))
echo server.call(counter, fn(reply) { Total(reply) }, 1000)
}
The message type is a class family, and requests that want an answer carry
a Subject for it. Both are checked by the compiler: a counter cannot be
sent a string, and Total can only be answered with an Int.
Underneath is a real gen_server, so the usual tools, such as observer
and sys:get_state/1, work as they do for Erlang servers.
API reference
class Server(msg)
A running server that accepts messages of type msg.
enum Action
What the server does before returning to ordinary mailbox processing.
Action.Wait
Action.Continue
Action.Hibernate
class Next(state)
What happens after a message: carry on with a new state, or stop.
let state: Option(state)
Option(state)let normal: Bool
Boollet reason: String
Stringlet action: Action
pooka/otp/server.Actionlet timeout: Option(Int)
Option(Int)new(
state: Option(state),
normal: Bool,
reason: String,
action: Action,
timeout: Option(Int)
)
fn(Option(state), Bool, String, pooka/otp/server.Action, Option(Int)) -> pooka/otp/server.Next(state)next(state: s) -> Next(s)
fn(a) -> pooka/otp/server.Next(a)Carry on, with this state.
stop() -> Next(s)
fn() -> pooka/otp/server.Next(a)Stop, because the work is done. A supervisor does not restart a
Transient child that stops this way.
fail(reason: String) -> Next(s)
fn(String) -> pooka/otp/server.Next(a)Stop, because something went wrong. The reason is logged, and a supervisor restarts the server.
class Spec(state, msg)
Everything needed to start a server. Make one with spec, then adjust it.
let init: fn(Subject(msg)) -> Result(state, String)
fn(pooka/otp.Subject(msg)) -> Result(state, String)let handle: fn(state, msg) -> Next(state)
fn(state, msg) -> pooka/otp/server.Next(state)let tidy: Option(fn(state, String) -> Nil)
Option(fn(state, String) -> Nil)let name: Option(Name(msg))
Option(pooka/otp.Name(msg))let init_timeout: Int
Intlet continuation: Option(fn(state) -> Next(state))
Option(fn(state) -> pooka/otp/server.Next(state))let timeout_handler: Option(fn(state) -> Next(state))
Option(fn(state) -> pooka/otp/server.Next(state))let initial_continue: Bool
Boolnew(
init: fn(Subject(msg)) -> Result(state, String),
handle: fn(state, msg) -> Next(state),
on_stop: Option(fn(state, String) -> Nil),
name: Option(Name(msg)),
init_timeout: Int,
continuation: Option(fn(state) -> Next(state)),
timeout_handler: Option(fn(state) -> Next(state)),
initial_continue: Bool
)
fn(fn(pooka/otp.Subject(msg)) -> Result(state, String), fn(state, msg) -> pooka/otp/server.Next(state), Option(fn(state, String) -> Nil), Option(pooka/otp.Name(msg)), Int, Option(fn(state) -> pooka/otp/server.Next(state)), Option(fn(state) -> pooka/otp/server.Next(state)), Bool) -> pooka/otp/server.Spec(state, msg)with_name(name: Name(msg)) -> Spec(state, msg)
fn(pooka/otp.Name(msg)) -> pooka/otp/server.Spec(state, msg)Register the server under a name, so it can be found with named, even
after a supervisor has restarted it.
on_stop(goodbye: fn(state, String) -> Nil) -> Spec(state, msg)
fn(fn(state, String) -> Nil) -> pooka/otp/server.Spec(state, msg)Run this when the server stops, with its last state and the reason. Use it to tidy up: close a file, say goodbye.
with_init_timeout(milliseconds: Int) -> Spec(state, msg)
fn(Int) -> pooka/otp/server.Spec(state, msg)How long init may take, in milliseconds. The default is five seconds.
on_continue(work: fn(state) -> Next(state)) -> Spec(state, msg)
fn(fn(state) -> pooka/otp/server.Next(state)) -> pooka/otp/server.Spec(state, msg)Configure work that runs before the next external mailbox message whenever a handler returns continue_with. Repeated continuations must eventually yield.
continue_on_start() -> Spec(state, msg)
fn() -> pooka/otp/server.Spec(state, msg)Request one continuation immediately after successful init.
on_timeout(work: fn(state) -> Next(state)) -> Spec(state, msg)
fn(fn(state) -> pooka/otp/server.Next(state)) -> pooka/otp/server.Spec(state, msg)Handle an idle timeout configured by next_with_timeout. Ordinary messages cancel that timeout, matching gen_server's inactivity semantics.
spec(
init: fn(Subject(m)) -> Result(s, String),
handle: fn(s, m) -> Next(s)
) -> Spec(s, m)
fn(fn(pooka/otp.Subject(a)) -> Result(b, String), fn(b, a) -> pooka/otp/server.Next(b)) -> pooka/otp/server.Spec(b, a)Describe a server by its two functions.
start(spec: Spec(s, m)) -> Result(Server(m), String)
fn(pooka/otp/server.Spec(a, b)) -> Result(pooka/otp/server.Server(b), String)Start a server, linked to this process. Returns once init has finished.
Inside a supervision tree, use supervisor.server instead, so the
supervisor starts it and can start it again.
named(name: Name(m)) -> Server(m)
fn(pooka/otp.Name(a)) -> pooka/otp/server.Server(a)The server registered under a name. Messages go to whichever process holds the name when they are sent, so this keeps working across restarts.
send(server: Server(m), message: m) -> Nil
fn(pooka/otp/server.Server(a), a) -> NilPost a message. This never waits.
call(
server: Server(m),
make: fn(Subject(r)) -> m,
timeout: Int
) -> Result(r, CallError)
fn(pooka/otp/server.Server(a), fn(pooka/otp.Subject(b)) -> a, Int) -> Result(b, pooka/otp.CallError)Send a request and wait for its answer. See otp.call.
subject(server: Server(m)) -> Subject(m)
fn(pooka/otp/server.Server(a)) -> pooka/otp.Subject(a)The server as a subject, for code that only knows how to send. Messages
sent to it are handled like send, and otp.send_after works too.
pid(server: Server(m)) -> Option(Pid)
fn(pooka/otp/server.Server(a)) -> Option(Pid)The process running the server now, if there is one.
shutdown(server: Server(m)) -> Nil
fn(pooka/otp/server.Server(a)) -> NilStop the server and wait until it has stopped. It stops normally, so its
on_stop runs.
continue_with(state: s) -> Next(s)
fn(a) -> pooka/otp/server.Next(a)Yield to the configured continuation before reading another mailbox message.
// Pair with spec.on_continue(fn(state) { server.next(state + 1) }).
server.continue_with(0)
hibernate(state: s) -> Next(s)
fn(a) -> pooka/otp/server.Next(a)Hibernate until the next message, allowing OTP to reduce an idle process heap.
next_with_timeout(state: s, milliseconds: Int) -> Next(s)
fn(a, Int) -> pooka/otp/server.Next(a)Run the configured idle handler if no ordinary message arrives in this time. Negative durations clamp to zero. Use otp.schedule for an independent deadline.
interface Handler(state, msg)
An object can describe a typed OTP server without exposing its callbacks as unrelated functions. Immutable handler classes keep init and handle work in the server process; actor implementations retain ordinary actor-call semantics.
class Counter(initial: Int) with server.Handler(Int, Message) {
init(inbox: Subject(Message)) -> Result(Int, String) { Ok(initial) }
handle(state: Int, message: Message) -> server.Next(Int) {
let _ = message
server.next(state)
}
}
// server.start(server.from_handler(Counter(10)))
init(inbox: Subject(msg)) -> Result(state, String)
handle(state: state, message: msg) -> Next(state)
from_handler(handler: Handler(s, m)) -> Spec(s, m)
fn(pooka/otp/server.Handler(a, b)) -> pooka/otp/server.Spec(a, b)Adapt an interface value into a normal Spec, preserving all the name, continuation, timeout and cleanup configuration methods.
The adapter invokes the handler synchronously. An actor handler moves the method body to its actor process: do not assume it owns the server's mailbox or monitors. Prefer an immutable class for process-local initialization.