# Awareness (Presence & Cursors)

Awareness is **ephemeral** collaboration state — who is in a room and where their
cursor is — that the server relays to the rest of the room. Unlike graph/text
operations, awareness is **never part of the CRDT and never persisted**: it lives
only in memory for the lifetime of each connection.

> **Note:** Use awareness for presence avatars, remote cursors/selections, "who's typing",
> and similar live-but-disposable signals. Use CRDT operations for anything that
> must survive a reconnect.

## How it differs from CRDT operations

| | CRDT operations | Awareness |
|---|---|---|
| Persisted | Yes (journal / snapshot) | No — in-memory only |
| Conflict handling | CRDT merge | Last write wins (no merge) |
| Relay | Sequenced, then broadcast | Relayed as-is to the room |
| Lifetime | Forever | Until the client disconnects |

## Wire messages

Two message types, defined in `packages/vuer-rtc/src/serdes.ts`:

```typescript
// A client's awareness state (or null to clear it, e.g. on blur / leave)
interface AwarenessState {
  user?: { id: string; name: string; color: string };
  cursor?: unknown;  // app-defined; a RelSelection is recommended (see "Relative cursor positions")
  ts?: number;
}

type WireMessage =
  // ...existing messages (crdt, broadcast, state, sync, ack, heartbeat, ...)
  // Client → server → other clients: publish or clear my state
  | { mtype: 'awareness'; client: string; state: AwarenessState | null }
  // Server → a newly-joined client: everyone already in the room
  | { mtype: 'awareness-roster'; states: Array<{ client: string; state: AwarenessState }> };
```

The `state` payload shape (`user`, `cursor`) is **owned by the application** — the
relay treats it as an opaque blob and never inspects it. `state: null` means the
client cleared its awareness (e.g. the editor lost focus) or disconnected.

## Client usage

Announce presence right after the WebSocket opens, then apply incoming updates:

```typescript

const client = crypto.randomUUID();  // one per connection — never reused/persisted
const roster = new Map();            // client → AwarenessState (OTHER clients only)

ws.onopen = () => {
  ws.send(serialize({
    mtype: 'awareness',
    client,
    state: { user: { id: 'alice', name: 'Alice', color: '#e11' }, ts: Date.now() },
  }));
};

ws.onmessage = (e) => {
  const msg = deserialize(new Uint8Array(e.data));
  switch (msg.mtype) {
    case 'awareness':
      if (msg.state) roster.set(msg.client, msg.state);
      else roster.delete(msg.client);
      break;
    case 'awareness-roster':
      for (const { client, state } of msg.states) roster.set(client, state);
      break;
  }
};

// On cursor move / selection change (throttle these sends):
ws.send(serialize({
  mtype: 'awareness',
  client,
  state: { user, cursor: { anchor, head }, ts: Date.now() },
}));

// On blur / leave — clear your caret for the room:
ws.send(serialize({ mtype: 'awareness', client, state: null }));
```

> **Note:** The server excludes you from your own room's fan-out and roster, so the roster
> only ever contains **other** clients. Identify a person by `state.user.id` and
> dedupe by it — one user may hold several connections (browser tabs).

## Relative cursor positions

vuer-rtc can express a cursor as a **relative position** — an anchor to a specific
character rather than a numeric offset — so it stays on the right spot as the
document is edited.

Every character in the text CRDT carries a **stable id**. A relative position
records the id of the character beside the caret; resolving it looks that
character up in the rope and returns its current offset. Ids are global and a
deleted character leaves a tombstone, so the same anchor resolves to the right
place as text changes around it and on every client. When the anchored character
is not present in a replica yet, resolution returns `null`.

The helpers live in `@vuer-ai/vuer-rtc`; their output goes inside the opaque
`AwarenessState.cursor` blob:

| Helper | Purpose |
|---|---|
| `encodePos(rope, offset)` | Caret offset → `RelPos` (anchored to the character on its left) |
| `resolvePos(rope, pos)` | `RelPos` → offset, or `null` if the anchor is not in this rope |
| `encodeSelection(rope, anchor, head)` | Selection offsets → `RelSelection` |
| `resolveSelection(rope, sel)` | `RelSelection` → `{ anchor, head }`, or `null` |

```typescript

const rope = getTextDocument(graph, 'content', 'text');

// Publish the local selection as stable anchors.
ws.send(serialize({
  mtype: 'awareness',
  client,
  state: { user, cursor: encodeSelection(rope, anchor, head), ts: Date.now() },
}));

// Render a peer's cursor against your current rope.
function drawPeerCursor(rope, peer) {
  const sel = resolveSelection(rope, peer.cursor); // { anchor, head } | null
  if (sel) render(peer.user, sel);
}
```

> **Note:** Resolve a peer's relative cursor whenever their document changes, not only when a
> new cursor message arrives — the anchor is stable, so re-resolving keeps the caret
> on its character as text is edited around it. See it in the
> [multiplayer cursor demo](/cursor-demo).

## Server behavior

The RTC server keeps a per-room, in-memory awareness map and:

1. **On an `awareness` message** — stores the sender's `state` (or deletes it when
   `null`) and relays the message to every *other* socket in the room. It never
   touches the journal.
2. **On connect** — sends the newcomer an `awareness-roster` containing everyone
   already present, so it can render the current room immediately.
3. **On disconnect** — drops the client's state and broadcasts
   `{ mtype: 'awareness', client, state: null }` so the others remove it.

Because it is pure in-memory relay, awareness works even when the server runs
without a database (journal-less relay mode).

## Backward compatibility

Awareness is additive and degrades cleanly:

- A server that predates it does not recognize the `awareness` message and drops
  it — no roster arrives, so a client shows no presence rather than erroring.
- Older clients ignore `awareness` / `awareness-roster` messages they don't handle.

## Reconnects

A client generates a **fresh `client` id on every (re)connect**. While
disconnected it may miss other clients' leave messages, so on reconnect it should
**reset its roster** and rebuild it from the `awareness-roster` the server sends —
rather than keep possibly-stale entries.
