Module

pooka/otp/server

src/pooka/otp/server.pooka

A 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:

  • init makes the first state. It is given the server's own subject, so the server can send itself messages later, such as timer ticks.
  • handle takes the state and one message, and says what happens next: carry on with a new state, stop, or fail.
Pooka
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

src/pooka/otp/server.pooka:52
class Server(msg)

A running server that accepts messages of type msg.

enum

Action

src/pooka/otp/server.pooka:57
enum Action

What the server does before returning to ordinary mailbox processing.

variant

Wait

src/pooka/otp/server.pooka:58
Action.Wait
variant

Continue

src/pooka/otp/server.pooka:59
Action.Continue
variant

Hibernate

src/pooka/otp/server.pooka:60
Action.Hibernate
class

Next

src/pooka/otp/server.pooka:64
class Next(state)

What happens after a message: carry on with a new state, or stop.

field

state

src/pooka/otp/server.pooka:65
let state: Option(state)
INFERREDOption(state)
field

normal

src/pooka/otp/server.pooka:66
let normal: Bool
INFERREDBool
field

reason

src/pooka/otp/server.pooka:67
let reason: String
INFERREDString
field

action

src/pooka/otp/server.pooka:68
let action: Action
field

timeout

src/pooka/otp/server.pooka:69
let timeout: Option(Int)
INFERREDOption(Int)
constructor

new

src/pooka/otp/server.pooka:71
new(
    state: Option(state),
    normal: Bool,
    reason: String,
    action: Action,
    timeout: Option(Int)
  )
INFERREDfn(Option(state), Bool, String, pooka/otp/server.Action, Option(Int)) -> pooka/otp/server.Next(state)
function

next

src/pooka/otp/server.pooka:87
next(state: s) -> Next(s)
INFERREDfn(a) -> pooka/otp/server.Next(a)

Carry on, with this state.

function

stop

src/pooka/otp/server.pooka:93
stop() -> Next(s)
INFERREDfn() -> pooka/otp/server.Next(a)

Stop, because the work is done. A supervisor does not restart a Transient child that stops this way.

function

fail

src/pooka/otp/server.pooka:99
fail(reason: String) -> Next(s)
INFERREDfn(String) -> pooka/otp/server.Next(a)

Stop, because something went wrong. The reason is logged, and a supervisor restarts the server.

class

Spec

src/pooka/otp/server.pooka:104
class Spec(state, msg)

Everything needed to start a server. Make one with spec, then adjust it.

field

init

src/pooka/otp/server.pooka:105
let init: fn(Subject(msg)) -> Result(state, String)
INFERREDfn(pooka/otp.Subject(msg)) -> Result(state, String)
field

handle

src/pooka/otp/server.pooka:106
let handle: fn(state, msg) -> Next(state)
INFERREDfn(state, msg) -> pooka/otp/server.Next(state)
field

tidy

src/pooka/otp/server.pooka:107
let tidy: Option(fn(state, String) -> Nil)
INFERREDOption(fn(state, String) -> Nil)
field

name

src/pooka/otp/server.pooka:108
let name: Option(Name(msg))
INFERREDOption(pooka/otp.Name(msg))
field

init_timeout

src/pooka/otp/server.pooka:109
let init_timeout: Int
INFERREDInt
field

continuation

src/pooka/otp/server.pooka:110
let continuation: Option(fn(state) -> Next(state))
INFERREDOption(fn(state) -> pooka/otp/server.Next(state))
field

timeout_handler

src/pooka/otp/server.pooka:111
let timeout_handler: Option(fn(state) -> Next(state))
INFERREDOption(fn(state) -> pooka/otp/server.Next(state))
field

initial_continue

src/pooka/otp/server.pooka:112
let initial_continue: Bool
INFERREDBool
constructor

new

src/pooka/otp/server.pooka:114
new(
    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
  )
INFERREDfn(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)
method

with_name

src/pooka/otp/server.pooka:136
with_name(name: Name(msg)) -> Spec(state, msg)
INFERREDfn(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.

method

on_stop

src/pooka/otp/server.pooka:151
on_stop(goodbye: fn(state, String) -> Nil) -> Spec(state, msg)
INFERREDfn(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.

method

with_init_timeout

src/pooka/otp/server.pooka:165
with_init_timeout(milliseconds: Int) -> Spec(state, msg)
INFERREDfn(Int) -> pooka/otp/server.Spec(state, msg)

How long init may take, in milliseconds. The default is five seconds.

method

on_continue

src/pooka/otp/server.pooka:180
on_continue(work: fn(state) -> Next(state)) -> Spec(state, msg)
INFERREDfn(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.

method

continue_on_start

src/pooka/otp/server.pooka:194
continue_on_start() -> Spec(state, msg)
INFERREDfn() -> pooka/otp/server.Spec(state, msg)

Request one continuation immediately after successful init.

method

on_timeout

src/pooka/otp/server.pooka:209
on_timeout(work: fn(state) -> Next(state)) -> Spec(state, msg)
INFERREDfn(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.

function

spec

src/pooka/otp/server.pooka:224
spec(
  init: fn(Subject(m)) -> Result(s, String),
  handle: fn(s, m) -> Next(s)
) -> Spec(s, m)
INFERREDfn(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.

function

start

src/pooka/otp/server.pooka:287
start(spec: Spec(s, m)) -> Result(Server(m), String)
INFERREDfn(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.

function

named

src/pooka/otp/server.pooka:319
named(name: Name(m)) -> Server(m)

The server registered under a name. Messages go to whichever process holds the name when they are sent, so this keeps working across restarts.

function

send

src/pooka/otp/server.pooka:326
send(server: Server(m), message: m) -> Nil
INFERREDfn(pooka/otp/server.Server(a), a) -> Nil

Post a message. This never waits.

function

call

src/pooka/otp/server.pooka:331
call(
  server: Server(m),
  make: fn(Subject(r)) -> m,
  timeout: Int
) -> Result(r, CallError)
INFERREDfn(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.

function

subject

src/pooka/otp/server.pooka:341
subject(server: Server(m)) -> Subject(m)

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.

function

pid

src/pooka/otp/server.pooka:348
pid(server: Server(m)) -> Option(Pid)
INFERREDfn(pooka/otp/server.Server(a)) -> Option(Pid)

The process running the server now, if there is one.

function

shutdown

src/pooka/otp/server.pooka:354
shutdown(server: Server(m)) -> Nil
INFERREDfn(pooka/otp/server.Server(a)) -> Nil

Stop the server and wait until it has stopped. It stops normally, so its on_stop runs.

function

continue_with

src/pooka/otp/server.pooka:376
continue_with(state: s) -> Next(s)
INFERREDfn(a) -> pooka/otp/server.Next(a)

Yield to the configured continuation before reading another mailbox message.

Pooka
// Pair with spec.on_continue(fn(state) { server.next(state + 1) }).
server.continue_with(0)
function

hibernate

src/pooka/otp/server.pooka:381
hibernate(state: s) -> Next(s)
INFERREDfn(a) -> pooka/otp/server.Next(a)

Hibernate until the next message, allowing OTP to reduce an idle process heap.

function

next_with_timeout

src/pooka/otp/server.pooka:387
next_with_timeout(state: s, milliseconds: Int) -> Next(s)
INFERREDfn(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

src/pooka/otp/server.pooka:405
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.

Pooka
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)))
method

init

src/pooka/otp/server.pooka:406
init(inbox: Subject(msg)) -> Result(state, String)
method

handle

src/pooka/otp/server.pooka:408
handle(state: state, message: msg) -> Next(state)
function

from_handler

src/pooka/otp/server.pooka:417
from_handler(handler: Handler(s, m)) -> Spec(s, m)

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.