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

Opaque handle to a running Lily application instance. Each runtime is isolated, allowing multiple independent apps on the same page.

pub opaque type Runtime(model, message)

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)
Search Document