Skip to content

Live events

Instead of polling, subscribe to a context’s real-time events. mero.events(contextIds:) opens a Server-Sent-Events stream and yields a ContextEvent for each emission — new messages, application-version flips, and other context activity — so a view can react as changes arrive.

events(contextIds:) returns an AsyncThrowingStream<ContextEvent, Error>. Consume it with for try await, inside a Task you can cancel:

let task = Task {
do {
for try await event in mero.events(contextIds: [contextId]) {
switch event.kind {
case "StateMutation":
await reloadMessages()
case "SyncStatus":
print("syncing: \(event.payload)")
default:
print("event on \(event.contextId): \(event.kind)")
}
}
} catch {
// the stream ended with an error (e.g. auth lost)
}
}

Each event is:

public struct ContextEvent: Sendable {
public let contextId: String
public let kind: String // "StateMutation" or "SyncStatus"
public let payload: JSONValue // raw event JSON
}

kind is the frame’s result.type, and a node sends exactly two: StateMutation (the context’s state moved) and SyncStatus (syncing / waitingForPeers / …). There is no ExecutionEvent — a case for one never fires.

The contract’s own events are nested a level down, under payload’s data.events[], each with its own kind and a data byte array carrying the encoded event. Captured from a live 0.11.0-rc.32 node:

{"result":{"contextId":"…","type":"StateMutation","data":{
"newRoot":"7320c55a…",
"events":[{"kind":"Inserted","data":[123,34,107,…],"handler":null}]}}}

So the frame tells you that state moved and the nested events[] tell you what the contract emitted.

The stream reconnects automatically ~3 seconds after a drop and re-subscribes to your context ids (the node persists the session’s subscriptions). You don’t need to rebuild the stream on a transient network blip — the for try await loop keeps delivering events across reconnects.

The stream’s lifetime is owned by the Task consuming it — cancel that task to close the SSE connection:

task.cancel() // closes the connection

.task ties the stream to a view’s lifetime — it’s started when the view appears and cancelled automatically when it disappears:

struct MessagesView: View {
let contextId: String
@State private var messages: [Message] = []
var body: some View {
List(messages) { Text($0.text) }
.task {
do {
for try await _ in mero.events(contextIds: [contextId]) {
messages = await load()
}
} catch { /* stream ended */ }
}
}
}