lily/server
The Server holds authoritative state and broadcasts updates
to connected clients. It works on both Erlang and JavaScript targets,
though we recommend Erlang for production use.
On Erlang, the server uses an OTP actor with sequential message
processing. On JavaScript, it uses closure-scoped mutable state (JS is
single-threaded). Both expose identical APIs - the same
Server opaque type and public functions work on both
targets.
The server owns the canonical Store, applies
client messages sequentially while assigning sequence numbers, broadcasts
updates to all clients except the originator, and sends full state
snapshots to clients that reconnect
import lily/server
import lily/store
import lily/transport
pub fn main() {
// Create your store
let app_store = store.new(initial_model, with: update)
// Start the server
let assert Ok(srv) = server.start(
store: app_store,
serialiser: transport.automatic(),
)
// Register side-effect hook (optional)
server.on_message(srv, fn(msg, model, client_id) {
case msg {
SaveDocument(doc) -> db.write(doc)
_ -> Nil
}
})
// Wire into your transport (mist/wisp WebSocket handler)
// See server/handler.gleam for examples
}
The server is transport-agnostic. It doesn’t depend on mist or wisp -
those are your backend dependencies. Use server.connect,
server.disconnect, and server.incoming
to wire the server into your WebSocket or HTTP handlers. See
lily/src/lily/server/handler.gleam for a complete example with mist
and wisp.
Note: within this module, “message” often refers to internal events, not your user-defined message type for model updates.
Types
Values
pub fn connect(
server: Server(model, message),
client_id client_id: String,
send send: fn(String) -> Nil,
) -> Nil
Register a client connection with the server. The send callback is how
the server pushes messages back to this specific client.
On Erlang, if you have a Subject(String) from mist’s WebSocket handler,
wrap it: send: process.send(outgoing_subject, _).
Example
// Erlang with mist WebSocket
let outgoing_subject = process.new_subject()
server.connect(srv, client_id: "abc123", send: process.send(outgoing_subject, _))
// JavaScript with Node.js WebSocket
server.connect(srv, client_id: "abc123", send: fn(text) { ws.send(text) })
pub fn disconnect(
server: Server(model, message),
client_id client_id: String,
) -> Nil
Unregister a client connection from the server. Called when a client disconnects.
pub fn incoming(
server: Server(model, message),
client_id client_id: String,
text text: String,
) -> Nil
Process an incoming message from a client. The text should be a serialised
transport.Protocol message (JSON string).
pub fn on_message(
server: Server(model, message),
hook: fn(message, model, String) -> Nil,
) -> Nil
Register a hook that runs after each client message is processed on the server. Receives the decoded message, updated model, and client id.
Example
server.on_message(server, fn(message, model, client_id) {
case message {
SaveDocument(doc) -> db.write(doc)
SendEmail(to, body) -> email.send(to, body)
_ -> Nil
}
})
pub fn start(
store store: store.Store(model, message),
serialiser serialiser: transport.Serialiser(model, message),
) -> Result(Server(model, message), Nil)
Start a new server instance with the given store and serialiser. Returns
Ok(server) on success, or Error(Nil) if the server fails to start
(Erlang actor init failure, though this is rare with simple init logic).
On JavaScript, this always returns Ok.
Example
import lily/server
import lily/store
let app_store = store.new(initial_model, with: update)
let assert Ok(srv) = server.start(store: app_store, serialiser: my_serialiser)