Durable Streams in MoonBit
Durable Streams is the resumable HTTP protocol around typed invocation stream slots. It differs
from generic agent RPC streaming: @schema.AgentStream[T] is the method value; Durable Streams
adds stable URLs for sessions and slots that external clients can append, resume, cancel, and fork.
Expose a Stream-Bearing Method
#derive.agent
#derive.mount("/pipes/{name}")
struct Pipe { name : String }
fn Pipe::new(name : String) -> Pipe { { name } }
#derive.endpoint(post="/uppercase")
#derive.durable_stream_slot(input="input", name="inbox")
#derive.durable_stream_slot(output="$result", name="outbox")
#derive.durable_streams(
allow_external_writes=true,
allow_stream_delete=true,
allow_invocation_delete=true,
max_concurrent_readers_per_stream=8,
max_append_requests_per_second_per_stream=25,
)
pub fn Pipe::uppercase(
self : Self,
input : @schema.AgentStream[String],
) -> @schema.AgentStream[String] {
// Produce lazily; read input and write transformed items.
...
}Slots are top-level stream parameters/outputs. $result selects a direct stream or scalar result;
an all-stream record output gets one slot per field. Public names replace canonical URL names.
Only direct @schema.AgentStream[Byte] slots may set content_type; other slots use JSON. Add the
agent to an httpApi deployment. Never edit generated .mbt files.
Paths are <base>, <base>/invocations/<session>, and
<base>/invocations/<session>/streams/<slot>; fork paths insert /forks/<fork>. PUT
creates/ensures, GET/HEAD reads, POST appends to writable inputs, and DELETE cooperatively cancels.
Create a session first if any non-stream method argument is not path/query-bound.
Protocol Rules
- JSON POST arrays are batches; wrap an array-valued message in another array. Byte slots use raw
bytes.
Stream-Closed: trueatomically appends and closes; empty means close-only. - Send
Producer-Id,Producer-Epoch, andProducer-Seqtogether and retry an uncertain append with exactly the same tuple and body. Offsets and cursors are opaque strings. - Read
offset=-1, or resolvenowonce and continue from the returned offset. A 204 long-poll or empty/up-to-date response is not EOF. Up-to-date plusStream-Closed: trueis EOF; missing, gone, cancelled, decoding, and transport outcomes are errors. - Slot DELETE tombstones it; session DELETE cancels open slots without interrupting arbitrary agent
work. Limits return 429/413; honor
Retry-Afterand do not retry oversized payloads unchanged. - Fork creation is a slot PUT with
Stream-Forked-Fromand optional offset/sub-offset, initial body, and closure. The source must have the same route/base, session ID, and public slot name as the target, either at the origin or in another fork. A normal Golem fork instead copies checkpoints and producer identity, so divergent branches can collide unless intentionally assigned independent producers.
Consume External Compatible URLs
Import golemcloud/golem_sdk/durable_streams:
async fn read_all(url : String) -> Array[String] {
let reader : @durable_streams.Reader[String] =
@durable_streams.json_reader(url)
defer reader.close()
let values = []
while reader.read() is Some(value) {
values.push(value)
}
values
}
async fn append_all(url : String, values : Array[String]) -> Unit {
let writer : @durable_streams.Writer[String] =
@durable_streams.json_writer(
url,
producer_id="orders-v1",
epoch=1,
)
defer writer.drop()
ignore(writer.append(values, close=true))
}Use byte_reader, byte_writer, and append_bytes for bytes; append and HTTP batch boundaries
disappear. Readers expose read, checkpoint, close, and one-time into_stream. Options cover
opaque offset/cursor, catch-up then LONG_POLL/SSE, attempt deadline, idle delay, and retries.
Writers retain one immutable uncertain producer tuple, encoded body, and close flag. Cancellation
does not erase it. Call retry_pending() before different data; exact acknowledgement alone
advances sequence, and an absent next_offset is valid. drop() releases locally without closing
or undoing a remote append.
Only timeout, transport, rate-limit, and unavailable failures retry with bounded exponential backoff, durable timers, and Retry-After. Protocol, fencing, conflict, gone, and size failures do not. Replay rebuilds checkpoints, buffered positions, pending data, producer progress, and retry budgets and does not repeat completed HTTP effects. Do not add custom durability/atomic wrappers.
Pass a raw @types.Secret capability as auth; the host pins it and sends the Bearer token. Never
reveal it or place credentials in the URL. HTTPS is required except when the host is localhost,
a dot-delimited subdomain such as app.localhost, or a loopback IP. Localhost names are
case-insensitive and may have one terminal DNS dot. The URL authority stays unchanged. Redirects
are rejected.
golem build
golem deploy --yes