Streams are for collections whose item values should not remain in server memory after delivery. Musubi keeps stream configuration and pending delta ops on the server, while the client owns the materialized list.
State Declaration
Declare streams inside state do. A stream may be top-level or nested under
plain object fields, but each stream name must be unique within one store.
state do
field :feed do
stream :messages, item_key: &"msg-#{&1.id}", limit: -100 do
field :id, String.t()
field :body, String.t()
end
end
stream :users, UserState.t(), item_key: &"user-#{&1.id}"
stream_async :older_messages, MessageState.t(), item_key: &"msg-#{&1.id}"
field :title, String.t()
endTop-level streams can use a named Musubi state module, a map type, or an inline block. Nested inline blocks are useful when the item shape belongs only to that store output.
Render Placement
render/1 must explicitly place each declared stream with stream(:name) at
the same path where it was declared.
def render(socket) do
%{
title: socket.assigns.title,
feed: %{messages: stream(:messages)},
users: stream(:users)
}
endFor async streams, use async_stream(:name). Musubi reads the current
AsyncResult from socket.assigns.name, or renders loading() before the
task has started:
def render(socket) do
%{
older_messages: async_stream(:older_messages)
}
endThe old placeholder shape, such as messages: [], is invalid. Musubi also
rejects hand-written stream marker maps, undeclared streams, missing stream
placements, duplicate placements, and placements at the wrong state path.
Wire Shape
The state tree carries only a marker at the stream path:
{
"title": "Inbox",
"feed": {
"messages": { "__musubi_stream__": "messages" }
},
"users": { "__musubi_stream__": "users" },
"__musubi_store_id__": []
}For async streams, the marker is nested under result:
{
"older_messages": {
"__musubi_async__": true,
"status": "loading",
"result": { "__musubi_stream__": "older_messages" },
"reason": null
}
}The marker needs only the stream name. Ownership and ordering metadata stay in
stream_ops, where each op carries store_id, stream, and ref.
{
"type": "patch",
"base_version": 0,
"version": 1,
"ops": [
{ "op": "replace", "path": "", "value": { "feed": { "messages": { "__musubi_stream__": "messages" } } } }
],
"stream_ops": [
{
"op": "insert",
"stream": "messages",
"ref": "1",
"store_id": [],
"item_key": "msg-1",
"at": -1,
"item": { "id": "1", "body": "hello" },
"limit": -100
}
]
}JSON Patch ops never carry stream item content. Stream item content flows
through stream_ops only.
Runtime API
The socket helpers mirror Phoenix LiveView streams, with item-key terminology:
stream(socket, name, items, opts \\ [])stream_configure(socket, name, opts)stream_insert(socket, name, item, opts \\ [])stream_delete(socket, name, item)stream_delete_by_item_key(socket, name, item_key)
Supported options include :item_key, :limit, :at, :reset, and
:update_only. stream_configure/3 must run before the stream is initialized.
After each render cycle, pending stream ops are drained into the patch envelope
and the server drops item values. A cycle with non-empty stream_ops emits an
envelope even when JSON Patch ops is empty.
Refresh
There is no separate reload API.
Use stream(socket, name, fresh_items, reset: true) when the application
already has fresh items. The runtime emits a reset followed by insert ops in
one envelope.
Use stream_async(socket, name, fun, reset: true) when the refresh requires a
background fetch and the UI should observe the async loading state. The runtime
writes socket.assigns.<name> as an AsyncResult, but the render helper keeps
the wire result as the stream marker so the client can expose
AsyncResult<Item[]>.
Client Surface
Generated TypeScript exposes stream fields as Musubi.StreamField<Item> for
type inference. At runtime, the client recursively resolves
{ "__musubi_stream__": "name" } markers into arrays:
root.feed.messages // Array<{ id: string; body: string }>
root.users // Array<UserState>
root.older_messages.data // Array<MessageState>Application code should not read or construct stream markers directly.