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.

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 operationsAwareness
PersistedYes (journal / snapshot)No — in-memory only
Conflict handlingCRDT mergeLast write wins (no merge)
RelaySequenced, then broadcastRelayed as-is to the room
LifetimeForeverUntil the client disconnects

Wire messages

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

// 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; e.g. { anchor, head } text offsets
  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:

import { serialize, deserialize } from '@vuer-ai/vuer-rtc';

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 }));

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

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.