Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Realtime

Listen

live var lines = Topic.For(<address>).Listen(); // stream<T> — appends as messages arrive

The receiving half of a topic, consumed on a page. Listen yields a stream of the topic's payloads, and a live var bound to it appends each message as it arrives — no re-query, no polling, and no attribute to opt a page in. The subscription opens when the component mounts and is released when it unmounts.

stable1 example compiled by CIrealtimetopicui

Summary#

Listen() is how a page receives from a topic. It yields a stream<T> of the topic's payloads; a live var bound to it holds each message as it arrives.

[Principal] entity User {
  [MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Room {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

entity Member {
  [Required] Room Room;
  [Required] User Person;
  security {
    allow read where Member.Any(m => m.Room == Room && m.Person == user);
    allow create where Person == user;
  }
}

class ChatLine { public string Body; }

topic RoomFeed(Guid roomId) {
  Candidates = u => Member.Any(m => m.Room.Id == roomId && m.Person == u);
  Carries    = ChatLine;
}

[Page("/room/{roomId}")]
[Render(CSR)]
component RoomPage(Guid roomId) {
  live var lines = RoomFeed.For(roomId).Listen();      // stream<ChatLine> — appends as they arrive

  render {
    foreach (var line in lines) { Text(line.Body); }
  }
}

Signature#

live var lines = Topic.For(<address>).Listen();   // stream<T>, appended to as messages arrive

Description#

There is nothing to opt in to#

A page participates in realtime by writing a subscription, and by nothing else. There is no attribute, no shell setting and no registration step: the live var above is the declaration, in the same way that declaring security on an entity is the whole of declaring it.

Listen yields a stream, deliberately#

Listen() returns a stream<T> — the same thing a live var already consumes from a streaming function. That is the point of the choice: a topic subscription needs no new rendering machinery, no new reconciler and no new foreach path, because it is the mechanism the UI already has, with a topic as its producer instead of a function.

It also means the ordinary rules apply. The subscription is opened when the component mounts and released when it unmounts; a reconnect re-establishes it; and the same page open twice on one topic is one subscription, not two.

A stream is observed, never awaited#

Listen() is legal in exactly one place: as the whole initializer of a live var. There is no single value to hold — the messages are still arriving — so var x = Topic.For(a).Listen(), await Topic.For(a).Listen() and live var n = Topic.For(a).Listen().Count are all refused, and say so.

What you are holding is a list — query it like one#

The live var a subscription is bound to is a collection that grows, so read it with the collection vocabulary you already have. foreach renders it, .Count says how many have arrived, and ordinary LINQ answers the rest:

live var nudges = Inbox.For(personId).Listen();

// …in the render:
foreach (var n in nudges.Reverse().Take(nudges.Count - dismissed)) { … }   // newest first, undismissed only
if (nudges.Any(n => n.Kind == NudgeKind.Mention)) { … }

Nothing re-reads anything to answer these — the messages are already in hand, and the chain runs where they are. .Done, .Failed, .Interrupted and .Error are the reads that are not about the items: whether the producer finished, and whether it stopped early. See component.

⚠ The one thing you may not do is query a subscription you have not bound. Topic.For(a).Listen().Where(…) is refused for the same reason await on it is: there is no list yet, only a producer. Bind it to a live var first.

The address is checked at the call site — and followed while the page lives#

A topic's parameters are its address, so RoomFeed.For(a) and RoomFeed.For(b) are two subscriptions. The address binds through the same argument rules as every other call in the language, and it is checked where you wrote it — because the only runtime symptom of a wrong address is a page that never updates, which is indistinguishable from a quiet room.

The subscription follows the address. If what the address reads changes — a state member, a parameter — the subscription is released and re-opened at the new one, and the stream is emptied first: what was said in the room you left is not part of the room you joined.

string room = "general";
live var lines = RoomFeed.For(room).Listen();   // switching `room` moves the subscription

A refused join is re-asked when data changes. Candidates is a question about your data, so a write is the only thing that can change its answer — and after one, every refused subscription asks again. A page that opens a room the reader may not enter yet therefore starts working the moment they are let in, with nothing to write for it.

⚠ What that does not do is re-run a plain query. A var read is a snapshot taken at mount by design, so a page that also shows history has to decide for itself whether entering is worth re-reading it.

What arrives is the value, not a signal#

Each message carries the publisher's payload, and the page renders it directly. It does not re-read the database: the subscription was admitted one principal at a time by the topic's Candidates rule, which is what makes carrying the value sound. That is the whole economic argument for topics — a room of ten people, a hundred messages deep, costs one message per viewer instead of a full re-query each.

A topic that carries nothing cannot be listened to#

Carries is what a subscriber binds. A signal-only topic has no value to deliver, so Listen() on one is a compile error naming the missing declaration rather than a subscription that delivers empty frames.

Examples#

The example above is compiled by the documentation gate.

For who is present on a topic rather than what was said on it, see Here, Announce.

See also#

Related

topic

A topic is a named, addressable realtime destination: a declared place to put a message so that every open page…

Here, Announce

Who is currently on a presence topic, and what they say they are doing. Here is the converging set of people present…

Realtime

Realtime in Osy# is one construct: a topic. A topic is a declared, addressable destination — publishing to one delivers…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…