# Listen

> 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.

<!-- id: realtime-listen · area: realtime · stability: stable · html: https://osysharp.com/reference/realtime/listen/ -->

## Summary        {#summary}

`Listen()` is how a page **receives** from a [topic](https://osysharp.com/reference/realtime/topic/). It yields a `stream<T>` of the topic's payloads;
a `live var` bound to it holds each message as it arrives.

```osy title="a live feed of a room" test app=realtime-listen
[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      {#signature}

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

## Description    {#description}

### There is nothing to opt in to   {#no-opt-in}

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   {#stream}

`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   {#not-awaitable}

`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   {#querying-a-subscription}

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:

```osy syntax
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](https://osysharp.com/reference/ui/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   {#address}

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.

```osy syntax
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   {#payload}

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   {#signal-only}

`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       {#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](https://osysharp.com/reference/realtime/presence/).

## See also       {#see-also}
- [topic](https://osysharp.com/reference/realtime/topic/) — the declaration, and publishing
- [Here, Announce](https://osysharp.com/reference/realtime/presence/) — who is here, and what they are doing
- [Realtime](https://osysharp.com/reference/realtime/index/) — realtime in one page
- [component](https://osysharp.com/reference/ui/component/) — the `live var` a subscription feeds
