lily/server

The Server holds authoritative state and routes client messages to the right store: a per-connection session store or a named topic store. It works on both Erlang and JavaScript targets, though Erlang is recommended for production.

Build a server with server.new, passing the shared Wiring configuration, then start it with server.start and register topics with topic.new:

import lily/server
import lily/topic

let assert Ok(srv) =
  server.new(
    initial: shared.initial_model(),
    serialiser: shared.serialiser(),
    wiring: shared.wiring(),
  )
  |> server.start

let assert Ok(chat) =
  topic.new(srv, id: "chat")
  |> topic.with_store

Wire the server into your WebSocket handler using server.connect, server.disconnect, and server.incoming.

Types

Handle to a running Lily server. Wraps platform-specific internals (OTP actor on Erlang, Reference cell on JavaScript). Also carries serialiser and initial_model for zero-copy access by topic.new.

pub opaque type Server(model, message)

Values

pub fn connect(
  server: Server(model, message),
  client_id client_id: String,
  send send: fn(BitArray) -> Nil,
) -> Nil

Register a client connection. The send callback is how the server pushes frames back to this specific client.

server.connect(srv, client_id: id, send: process.send(outgoing_subject, _))
pub fn disconnect(
  server: Server(model, message),
  client_id client_id: String,
) -> Nil

Unregister a client connection from the server and all subscribed topics.

pub fn generate_client_id() -> String

Generate a cryptographically random 32-character hex client identifier. Pair with connect so every connection carries a stable, server-issued id.

let client_id = server.generate_client_id()
server.connect(srv, client_id:, send:)
pub fn incoming(
  server: Server(model, message),
  client_id client_id: String,
  bytes bytes: BitArray,
) -> Nil

Process an incoming frame from a client. Decodes the frame and routes it: SessionMessage to the session store; TopicMessage, Subscribe, and Unsubscribe to the topic actor; Resync to a per-target snapshot fan-out.

pub fn new(
  initial initial: model,
  serialiser serialiser: transport.Serialiser(model, message),
  wiring wiring: store.Wiring(model, message),
) -> @internal Builder(model, message)

Start building a server. Provide the shared initial model (used as the zero-state for per-connection session stores and for topic snapshot construction) and the serialiser.

server.new(
  initial: shared.initial_model(),
  serialiser: shared.serialiser(),
  wiring: shared.wiring(),
)
pub fn on_message(
  server: Server(model, message),
  hook: fn(message, model, String) -> Nil,
) -> Nil

Register a hook that runs after each session message is applied. Receives the decoded message, the updated session model (projected from the outer model), and the originating client id.

server.on_message(srv, fn(message, model, client_id) {
  case message {
    Session(SaveDocument(doc)) -> db.write(doc)
    _ -> Nil
  }
})
pub fn on_topic_message(
  server: Server(model, message),
  hook: fn(message, String, String) -> Nil,
) -> Nil

Register a hook that runs for each client-incoming topic message. Receives the decoded message, the topic id, and the originating client id. Fires before the topic actor processes the message, regardless of whether the topic is stateful, ephemeral, or unknown. Does not fire for server-initiated topic.dispatch / topic.broadcast calls.

server.on_topic_message(srv, fn(message, topic_id, _client_id) {
  logging.auto_log(logging.Info, #(topic_id, message))
})
pub fn start(
  builder: @internal Builder(model, message),
) -> Result(Server(model, message), Nil)

Materialise the configured server. Topics are added afterwards via topic.new(server, ...).

let assert Ok(srv) =
  server.new(
    initial: shared.initial_model(),
    serialiser: shared.serialiser(),
    wiring: shared.wiring(),
  )
  |> server.start
pub fn stop(server: Server(model, message)) -> Nil

Stop a running server. Every registered topic actor is asked to stop first; each subscriber receives a final Acknowledge(Topic(id), seq) so client slices reset cleanly. The underlying server actor then terminates (Erlang) or its Reference state cell is cleared (JavaScript). Connected session clients receive no extra frame.

Search Document