Streamed Calls
An ordinary call carries values: an input goes in whole, and an output comes back whole. Some work does not fit that shape. A report is produced row by row. A video is transcoded while it is still being uploaded. Carrying such payloads as values would mean holding all of it in memory at every step.
Calls can therefore carry streams, in either direction. The two directions have very different consequences.
§Streamed replies
An operation may answer with a stream instead of a value:
export async function* observation (input: Input, entries: Entity[]) {
for (const order of entries)
yield format(order)
} The caller receives a stream and reads what the operation produces, as it is produced. This changes nothing else about the call. It is addressed, queued, delivered, and traced like any other, and any transport can carry it.
A reply stream that ends was written to its end. If the operation fails partway, or the process serving it stops, the caller’s stream fails too. A truncated stream is never presented as a complete one.
§Streamed input
An operation may also take a stream. It names the input property that carries it:
operations:
checksum:
type: computation
stream: content export async function computation (input: Input) {
return await digest(input.content)
} const sum = await context.remote.files.checksum({ input: { content: stream } }) The operation reads what the caller writes while the caller is writing it. Nothing in between holds the payload.
§What streamed input gives up
Almost everything the runtime does for an ordinary call depends on the call being a message: a finite value that can be stored in a queue and delivered again. A stream in progress is not such a value. It exists only while its writer is writing it.
A streamed call is therefore a different kind of call, and gives up what the others have:
| Ordinary call | Streamed call |
|---|---|
| Waits for a component that is absent. | Fails if no replica is running. |
| Is delivered again if the process serving it stops. | Is not retried; a redelivery would carry nothing. |
| Can be made as a task or delayed. | Cannot be deferred. |
| Shares connections. | Takes a connection of its own for its duration. |
It can also be declared only on operations the runtime never runs twice for one call: computations and effects. A transition may be re-run on a concurrent change, and a stream cannot be read a second time.
Within one composition none of this applies. A stream passed between components in the same process is passed directly.
§When it is the right tool
A streamed call is for bytes that have to flow through an operation: a checksum, a transcode, a parse, a forward. It is the wrong tool for three things that resemble it.
- A large input that is a value is still a value, and travels as an ordinary call.
- An answer that arrives in pieces is a streamed reply, which costs nothing extra.
- An upload that is to be kept is stored by the platform as it arrives, and components are called afterwards with a reference to it; see Files.
Used for any of these, a streamed call pays the price in the table above and buys nothing.
§The general point
Streams show where the runtime’s guarantees come from. Durability, retries, and deferral are properties of messages. A call keeps them for as long as it can be treated as one. The model lets an application leave that ground when the work demands it, and makes the exchange explicit in the manifest, so the cost is visible where the choice is made.