lily/client
The Runtime is the core of every Lily application running
in the browser. It manages the update loop, component subscriptions, and
optional server synchronisation.
The runtime routes messages from events and the server to your update
function, notifies subscribed components when the model changes, and
optionally connects to a server to sync state across clients (see
client.connect). When connected, it monitors online/
offline status and queues messages in localStorage while disconnected.
The typical flow: create a store with store.new,
start the runtime with client.start, mount your UI using
component.mount, attach event handlers with
event.on_click and friends, then optionally
connect to a server with client.connect
import lily/client
import lily/component
import lily/event
import lily/store
import lily/transport/websocket
pub fn main() {
// 1. Create your store
let app_store = store.new(Model(count: 0), with: update)
// 2. Start the runtime
let runtime = client.start(app_store)
// 3. Mount your UI
runtime
|> component.mount(selector: "#app", to_html: element.to_string, view: app)
// 4. Attach events
|> event.on_click(selector: "#app", decoder: parse_msg)
// 5. Connect to server (optional)
|> client.connect(
with: websocket.config(url: "ws://localhost:8080/ws") |> websocket.connect,
serialiser: my_serialiser,
)
}
Each Runtime is completely isolated, allowing multiple
independent Lily apps to coexist on the same page. However, we recommend
using one runtime per page to avoid splitting your application state.
If you need truly independent widget-style components, a different
framework may be more appropriate.
The runtime is pure JavaScript and works only on the
@target(javascript) platform. It uses a message queue to batch updates
and prevent race conditions, ensuring your update function is called
sequentially even when messages arrive from multiple sources (user
events, server messages, timers, etc.).
Types
Values
pub fn connect(
runtime: Runtime(model, message),
with connector: fn(transport.Handler) -> transport.Transport,
serialiser serialiser: transport.Serialiser(model, message),
) -> Runtime(model, message)
Connect the runtime to a server using the provided transport. The
connector function is obtained from a transport implementation (e.g.,
websocket.connect(config) or http.connect(config)).
Example
import lily/transport/websocket
runtime
|> client.connect(
with: websocket.config(url: "ws://localhost:8080/ws")
|> websocket.reconnect_base_milliseconds(2000)
|> websocket.connect,
serialiser: my_serialiser,
)
pub fn connection_status(
runtime: Runtime(model, message),
get get: fn(model) -> Bool,
set set: fn(model, Bool) -> model,
) -> Runtime(model, message)
Often times you want to be able to track the connection status (for
example, if you want to disable an element when there is no connection).
This sets up tracking for the connection status in the model, with Lily
calling set with True when the transport connects and False when it
disconnects. Components can slice this field to react to connectivity
changes.
get provides the way to read the connection status from the model (the
user-defined model type should then have a way to save this status) and
set provides a way to write into the model.
This should be called before client.connect to ensure the
initial connection state is captured.
Example
runtime
|> client.connection_status(
get: fn(model) { model.connected },
set: fn(model, status) { Model(..model, connected: status) },
)
|> client.connect(
with: websocket.connect(config),
serialiser: my_serialiser,
)
pub fn dispatch(
runtime: Runtime(model, message),
) -> fn(message) -> Nil
Get a dispatch function that sends messages into the runtime’s update loop. Use this for side effects that need to feed results back as messages (fetch callbacks, timers, external listeners).
Example
let runtime = client.start(store)
let dispatch = client.dispatch(runtime)
fetch("/api/data", fn(response) {
dispatch(DataReceived(response))
})
pub fn on_message(
runtime: Runtime(model, message),
hook: fn(message, model) -> Nil,
) -> Nil
Register a hook that runs after each locally-dispatched message. Does not fire for remote messages from other clients.
Example
let dispatch = client.dispatch(runtime)
client.on_message(runtime, fn(message, model) {
case message {
FetchUsers -> fetch("/api/users", fn(users) {
dispatch(UsersLoaded(users))
})
_ -> Nil
}
})
pub fn start(
store: store.Store(model, message),
) -> Runtime(model, message)
Start the client runtime. Returns a Runtime handle that should be used
with component.mount, event handlers, and
optionally client.connect.
Example
let runtime =
store.new(Model(count: 0), with: update)
|> client.start
runtime
|> component.mount(selector: "#app", to_html: element.to_string, view: app)
|> event.on_click(selector: "#app", decoder: parse_msg)