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 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:
// 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:
- On an
awarenessmessage — stores the sender'sstate(or deletes it whennull) and relays the message to every other socket in the room. It never touches the journal. - On connect — sends the newcomer an
awareness-rostercontaining everyone already present, so it can render the current room immediately. - 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
awarenessmessage and drops it — no roster arrives, so a client shows no presence rather than erroring. - Older clients ignore
awareness/awareness-rostermessages 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.