Summary#
Most functions answer once: you call them, they think, they return. Some answers do not work that way — an assistant's reply arrives a word at a time, a log tail never finishes at all, an import of fifty thousand rows has something useful to say long before it is done.
A stream<T> function produces its results one at a time:
stream<string> Tail(string path) {
foreach (var line in Lines(path)) {
yield return line;
}
}
string[] Lines(string path) { return [path]; }A component binds it with an ordinary live var, and renders each item as it arrives:
component LogView(string Path) {
live var lines = Tail(Path);
render {
Stack(overflowY: Overflow.Auto, stickToBottom: true) {
foreach (var line in lines) { Text(line); }
}
}
}Nothing polls, nothing re-fetches, and no item is rendered twice.
Signature#
stream<T> Name(args) { … yield return item; … } // declare a producer
live var items = Name(args); // observe it; items appear as they arrive
foreach (var item in items) { … } // renders each one, once
items.Count // how many so far
items.Failed · items.Error // it stopped early, and why
items.Interrupted // …and it was the CONNECTION that dropped, so a retry may work
items.Done // the producer finishedDescription#
What yield return does#
yield return x; hands one item to the caller and keeps going. The function does not end — the next statement
runs, and the next yield return delivers the next item. When the function reaches its end, the stream is complete.
entity Document { [MaxLength(200)] string Title; [MaxLength(4000)] string Body; }
class Match { public string Title; public string Snippet; }
stream<Match> Search(string term) {
foreach (var doc in Document.Where(d => d.Body.Contains(term))) {
yield return new Match { Title = doc.Title, Snippet = doc.Body };
}
}To stop early, return; on its own — the stream completes normally, with whatever it produced so far.
stream<string> FirstPage(string path) {
var n = 0;
foreach (var line in Lines(path)) {
if (n >= 100) { return; } // enough — complete the stream
n = n + 1;
yield return line;
}
}
string[] Lines(string path) { return [path]; }Coming from C#? There is no
yield breakhere. C# needs it because a barereturn;in an iterator is ambiguous with returning a value; astream<T>function never returns a value, soreturn;is unambiguous and means exactly whatyield breakmeans in C#. Everything else is the same, includingyield returnitself.
A stream is observed, never awaited#
A stream<T> can only be bound to a live var. Calling one anywhere else is a compile error, because there is no
"the whole thing" to hold — the answer is still arriving:
var lines = Tail(path); // ✗ a stream has no single value to assign
on mount { Tail(path); } // ✗ same reason
live var lines = Tail(path); // ✓This is the same distinction The reactivity & lifecycle model already draws. A live var is a value binding — it says what a
value is, continuously — and a stream is exactly that: a collection that is still being written.
Why an ordinary server function cannot be a live var#
The reactivity & lifecycle model refuses live var files = FilesInFolder(id); because nothing subscribes that value to anything —
it would be fetched once and then quietly go stale, or force a hand-off to the server mid-render.
A stream removes that objection rather than working around it: the stream itself is the subscription. The server
holds the connection open and pushes; there is nothing to poll and nothing to invalidate. That is why stream<T> is
allowed exactly where an ordinary server call is not.
Items only ever arrive — they are never revised#
A stream is append-only. There is no way to change or remove an item once it has been yielded, and that is a guarantee rather than a missing feature: it is what producers actually do (an assistant never un-says a word, a log never un-writes a line), and it is what keeps rendering cheap. Appending touches the end of the list, so the items already on screen are left alone — see Markdown — rendering markdown text, whose renderer keeps the DOM of every block that did not change.
If you need to replace a value as it evolves, that is an ordinary reactive read, not a stream.
How does a stream end, and how do I tell which way?#
Four ways, and a component can tell them apart:
Done | Failed | Interrupted | what to show | |
|---|---|---|---|---|
| the function ended | true | false | false | the finished list |
| the function raised an error | false | true | false | the items so far, plus Error |
| the connection dropped | false | true | true | the items so far, and an offer to retry |
| still producing | false | false | false | the items so far, and usually a spinner |
Interrupted narrows Failed; it does not replace it. A dropped connection is both, so a component that only
checks Failed still shows something — where two mutually exclusive flags would leave it waiting forever on a drop.
A dropped connection does not stop the producer, and the platform reconnects for you. The producer's life is its
own: it keeps running on the server while the browser is away, and reconnecting resumes reading the same run from
the item you already have. Nothing is re-run, so item 40 is the same item 40 — which is what makes reconnecting
automatic rather than a way to splice the first half of one answer onto the second half of another. A few attempts
are made, backing off; Interrupted is what you hear when they are spent, so it means "this is not coming back",
not "the connection blinked".
Offering a retry is therefore about starting again, and that is meaningful only when re-running the producer would produce the same items. A tail of a file or a read of stored rows will; an assistant's reply will not, because a second run writes a different answer — so an app that streams replies is usually better asking the question again than resuming it.
Leaving stops it. A component that unmounts, or a page that navigates away, tells the server it is going — the producer stops immediately, rather than running on for the grace window that covers a genuine drop.
An answer that finished while you were away is kept. If the connection dropped and the producer went on to finish with nobody reading, the platform keeps that answer so the reader can still collect it — across a restart, not only for the few minutes it stays in memory. How long, and how much, are the platform's to decide: there is no attribute to write and no knob to get wrong.
A stream that fails keeps everything it already produced. A reply that broke off halfway still said what it said, and the reader has already read it — discarding it would destroy the only record of how far it got.
stream<string> Ask(string question) { yield return question; }
component Answer(string Question) {
live var reply = Ask(Question);
render {
Stack {
foreach (var part in reply) { Markdown(part, streaming: !reply.Done); }
if (reply.Failed) { Text(reply.Error); }
else if (!reply.Done) { Text("…"); }
}
}
}Where it runs#
stream<T> is a server producer — that is decided by the declaration itself, not by anything you write — and
its items cross to the browser as they are produced, over the caller's own connection. Items go to the component that asked for them and to nothing else — a stream is never broadcast, and one
visitor's results are never visible to another.
Everything a stream reads obeys the same rules any server read does. There is nothing extra to declare and nothing extra to check.
Streaming markdown#
The common case for a text stream is markdown that is still being written, which Markdown — rendering markdown text handles directly:
live var reply = Ask(question);
render {
Stack(overflowY: Overflow.Auto, stickToBottom: following) {
foreach (var part in reply) {
Markdown(part, streaming: !reply.Done);
}
}
}streaming: holds a half-typed construct together so the reader never sees raw markdown syntax, and
stickToBottom: follows the new text without yanking a reader who has scrolled up (layout primitives).
See also#
- The reactivity & lifecycle model —
live var, and what may initialize one - Markdown — rendering markdown text — rendering a stream of markdown as it arrives
- layout primitives —
stickToBottom, for a surface that grows while you read it