lily/store

The Store holds your application state and update logic, shared across client and server. The Wiring tells the runtime how to dispatch messages to the right store slice and how to merge incoming snapshots from the server.

Build a Wiring in your shared package and import it in both the client (passed to client.start) and the server (passed to server.new):

import lily/store

pub fn wiring() -> store.Wiring(Model, Message) {
  store.wiring()
  |> store.session(
    extract: fn(message) {
      case message {
        Session(inner) -> Ok(inner)
        _ -> Error(Nil)
      }
    },
    update: session_update,
    field_get: fn(model: Model) { model.session },
    field_set: fn(model, session) { Model(..model, session:) },
  )
  |> store.topic(
    id: "chat",
    extract: fn(message) {
      case message {
        Chat(inner) -> Ok(inner)
        _ -> Error(Nil)
      }
    },
    update: chat_update,
    field_get: fn(model: Model) { model.chat },
    field_set: fn(model, chat) { Model(..model, chat:) },
  )
}

Model fields that should not be synced to the server can be wrapped in Local. The server holds Local fields at their initial values and the client runtime preserves them when the server sends a snapshot on reconnect. Pair with client.session_field to persist them across page navigations.

The same store runs on the client via client.start and the server via server.start, meaning your update function works identically on both sides.

Types

Model fields that are client-only and not synced to the server should be wrapped using Local(_). The server holds Local fields at their initial values and the client runtime preserves them when applying a server snapshot on reconnect.

pub type Model {
  Model(count: Int, theme: store.Local(String))
}
pub type Local(a) {
  Local(a)
}

Constructors

  • Local(a)

The store with your application state and update logic. The same store runs on both the client (via client.start) and the server (via server.start). Construct via new; fields are not exposed to keep the internal layout free to evolve.

pub opaque type Store(model, message)

Wiring configuration for multi-store Lily apps. A Wiring(model, message) value tells the client how to dispatch messages to the session store or to a topic store, and how to merge incoming snapshots back into the outer model. Build with wiring, then pipe through session and topic.

pub opaque type Wiring(model, message)

Values

pub fn new(
  initial_model model: model,
  with update: fn(model, message) -> model,
) -> Store(model, message)

Create a new store, seeded with an initial_model (similar to Lustre’s init) and an update function that transforms the model based on a given message.

let app_store =
  Model(count: 0, user: "Guest")
  |> store.new(with: update)
pub fn session(
  wiring: Wiring(model, message),
  extract extract: fn(message) -> Result(session_message, Nil),
  update update: fn(session_model, session_message) -> session_model,
  field_get field_get: fn(model) -> session_model,
  field_set field_set: fn(model, session_model) -> model,
) -> Wiring(model, message)

Register the session store entry in the wiring. The extract function identifies session messages; update applies them to the session sub-model; field_get and field_set map between the outer model and the session sub-model.

store.wiring()
|> store.session(
  extract: fn(message) {
    case message {
      Session(inner) -> Ok(inner)
      _ -> Error(Nil)
    }
  },
  update: session_update,
  field_get: fn(model: Model) { model.session },
  field_set: fn(model, session) { Model(..model, session:) },
)
pub fn topic(
  wiring: Wiring(model, message),
  id id: String,
  extract extract: fn(message) -> Result(topic_message, Nil),
  update update: fn(topic_model, topic_message) -> topic_model,
  field_get field_get: fn(model) -> topic_model,
  field_set field_set: fn(model, topic_model) -> model,
) -> Wiring(model, message)

Register a topic store entry in the wiring. The id is the topic identifier used in client.subscribe; the other parameters are the same as for session.

store.wiring()
|> store.topic(
  id: "chat",
  extract: fn(message) {
    case message {
      Chat(inner) -> Ok(inner)
      _ -> Error(Nil)
    }
  },
  update: chat_update,
  field_get: fn(model: Model) { model.chat },
  field_set: fn(model, chat) { Model(..model, chat:) },
)
pub fn unwrap_local(local: Local(a)) -> a

Unwrap a Local field to get the inner value.

pub fn wiring() -> Wiring(model, message)

Create an empty wiring configuration. Pipe through session and topic to register stores. Pass the result to client.start and server.new.

store.wiring()
|> store.session(extract:, update:, field_get:, field_set:)
|> store.topic(id: "chat", extract:, update:, field_get:, field_set:)
Search Document