Acore language

[ AxiomCore · Documentation ]

Streams and WebSockets

Declare HTTP streams, server-sent events, WebSockets and file streams on an endpoint, and bound how a frontend view keeps stream data.

Attach one StreamingDef subtype to an endpoint to declare an HTTP stream, server-sent events (SSE), a WebSocket or a file stream. This page lists those declarations, the options that bound a frontend stream view, and the durable batch-sink profile. Backend examples were checked with axiom check from CLI 0.149.1.

The executable action host supports typed SSE, JSON or message HTTP streams and bidirectional JSON WebSockets with explicit limits and per-message authorization. See Integrations and transports for source policies, provider bindings and transport boundaries.

Declarations

streaming(transport: StreamingDef) goes inside an endpoint. Pass one of these constructors.

ConstructorArguments and defaults
HttpStreamDefresponseType: TypeRef?, framing = "chunk" (or "event", "message", "file"), encoding = "json" (or "text", "binary")
SseStreamDefresponseType: TypeRef?. The kind is fixed to sse.
WebSocketDefdirection = "bidirectional" (or "server_to_client"), reconnect = "auto" (or "manual", "none"), client: WsMessageDef?, server: WsMessageDef?
WsMessageDefmodel, required: a model reference or Json
FileStreamDefcontentType: String?, filename: String?, disposition = "attachment" (or "inline")

An unsupported value is rejected with AXB002, for example Expected "chunk" | "event" | "message" | "file".

backend/main.acore
project(id: "stream-example", version: "1.0.0")
model Event { id: String }
model ChatMessage { text: String }
model ChatResponse { text: String }
endpoint events(method: GET, path: "/events") {
  response(type: Models.Event)
  streaming(HttpStreamDef(responseType: Models.Event, framing: "event", encoding: "json"))
}
endpoint sse(method: GET, path: "/sse") {
  response(type: Models.Event)
  streaming(SseStreamDef(responseType: Models.Event))
}
endpoint chat(method: WS, path: "/chat") {
  response(type: Models.ChatResponse)
  streaming(WebSocketDef(
    direction: "bidirectional",
    reconnect: "auto",
    client: WsMessageDef(model: Models.ChatMessage),
    server: WsMessageDef(model: Models.ChatResponse)
  ))
}
endpoint report(method: GET, path: "/report") {
  response(type: Bytes)
  streaming(FileStreamDef(
    contentType: "application/pdf", filename: "report.pdf", disposition: "attachment"
  ))
}

client and server each take a single WsMessageDef, not a list.

Bounded frontend views

An Acore UI stream can bound how much data its view keeps. The CLI 0.149.1 compiler validates these options. Use a matching compiler and host: new builds require acore-data/v3, and an older host cannot silently ignore the options. Editor and mounted-host qualification are separate.

OptionAllowed valuesBehavior
buffer1 to 10,000 itemsItems the view keeps. The reduce mode is append, latest or latest_by; latest_by needs a typed String or Int event key.
overflow"error" or "drop_oldest"What happens at capacity
max_bytes64 to 1,048,576Bound on the UTF-8 JSON size of retained items
idle_timeout10ms to 1hOwner-scoped deadline, reset by each event and cleared on close, disposal or session change
sequenceA field name stringA public Int event field holding the source's increasing sequence
map, filterA pure function namemap(Event) -> Event, filter(Event) -> Bool
foldA pure function name(List<Event>, List<Event>) -> List<Event> over the current view and a batch
window_items1 to 256Flush a fold window after this many items
window_time10ms to 1mOptional flush deadline; needs window_items
  • overflow: "error" stops before the view exceeds its item capacity.
  • overflow: "drop_oldest" makes the retained view lossy on purpose: dropped counts removed items and gap records the loss. This is a presentation policy, not a durable event-processing or sink policy. Required business events cannot use it. Older policies keep their earlier bounded-view behavior.
  • sequence must not name a task ID or other business key. After the first event, a duplicate, out-of-order or missing sequence number stops the subscription before publication and sets gap. Automatic reconnect cannot repair missing history: refresh from an authoritative snapshot before you open a new subscription.
  • map and filter receive explicit event values with the normal pure evaluator's fuel and value limits. Sequence and cursor progress follow the original event even when a filter drops it; a map cannot rewrite transport progress.

Folds and windows

A fold reduces the current view and an ordered batch of events. The initial view is an empty typed list, and the fold returns the same list type. It cannot capture page, session or store state.

frontend/main.acore excerpt
module advanced.windows.ui
use contract Workflow from "workflow" audience "mobile"
model Change { id: Int; title: String; cursor: String }
pure fn replaceWindow(state: List<Change>, batch: List<Change>): List<Change> = batch
page Home {
  stream updates = Workflow.updates(after: null) options(
    fold: replaceWindow,
    window_items: 2,
    window_time: 100ms,
    buffer: 4,
    max_bytes: 65536,
    overflow: "error",
    sequence: "id"
  )
  view { Text(updates.status) Text(to_string(updates.items.length)) }
}

This excerpt needs an updates(after: String?) -> Change operation in the contract. Its id stands in for a contiguous transport sequence; in your application, select a real transport sequence field, because an entity ID does not establish stream ordering.

  • A fold requires an explicit max_bytes and overflow: "error". Window options require a fold.
  • A count window flushes at window_items. An optional window_time starts when the first item enters an empty window and stays fixed as more items arrive. The host owns the timer.
  • A natural close flushes the remaining batch. Cancellation, session replacement and disposal discard unpublished items and clear the timer.
  • The queue and the resulting view each respect max_bytes, and the view also respects buffer. Fold overflow is terminal and never silently drops required events.

Count and time windows are frontend presentation windows. They do not start background workers or provide durable upstream history. Test subscription ordering, overflow, cancellation, reconnect and disposal against your selected transport; synthetic transport checks do not prove mounted native delivery or durable provider behavior.

Durable batch sinks

The native PostgreSQL profile reuses the event, publisher and consumer declarations, a managed transaction and typed resource operations. There is no separate sink or pipeline keyword.

  • A source publishes an immutable, bounded batch with a stable receipt key, and a trusted worker consumes it through a guarded action.
  • In the reference profile a batch has 1 to 32 items and each title has a 200-character bound. The private runtime specification binds the batch write to checked batch-insert steps and one receipt in the same serializable transaction. See Integrations and transports for the private provider contract.
  • A worker acknowledges the queue only after the action succeeds. If the process dies after the database commit but before the acknowledgment, the queue may deliver the same message again; the unchanged batch key and input replay the database receipt. Changed input for an existing key is a conflict. Keep keys stable across redelivery.
  • Queue retention and database receipt retention are separate responsibilities. This does not make an arbitrary external publication transactional or give a general exactly-once guarantee.
  • Activation needs a signed, qualified database schema, a least-privilege application role, exact resource grants, explicit service credentials and the durable Redis configuration. Queue ordering applies to the declared binding.