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
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.