Presence tracks ephemeral per-client state with a TTL. Entries expire automatically when a client stops sending heartbeats. Ideal for “who’s online”, cursor positions, and typing indicators.
Usage without schema
const room = client.presence("pr:room-123");
// Announce presence (data: any, ttl in ms)
room.heartbeat({ status: "online" }, 30_000);
// Read current entries
console.log(room.value()); // Map<clientId, { data: unknown; expiresAtMs: number }>
// Subscribe
room.onChange(entries => {
for (const [clientId, entry] of entries) {
console.log(clientId, entry.data);
}
});
// Leave
room.leave();
Usage with schema
import { Schema } from "effect";
const Cursor = Schema.Struct({
x: Schema.Number,
y: Schema.Number,
});
const cursors = client.presence("pr:cursors", Cursor);
cursors.heartbeat({ x: 120, y: 340 }, 5_000);
cursors.onChange(entries => {
for (const [clientId, entry] of entries) {
// entry.data: { x: number; y: number }
renderCursor(clientId, entry.data.x, entry.data.y);
}
});
API
| Method | Description |
|---|
heartbeat(data: T, ttlMs: number) | Send presence data, expires after ttlMs |
leave() | Remove own entry immediately |
value() | Returns Map<number, PresenceEntry<T>> — only live entries |
onChange(fn) | Subscribe — returns unsubscribe function |
PresenceEntry
interface PresenceEntry<T> {
data: T;
expiresAtMs: number; // absolute timestamp
}
TTL behavior
Entries expire when now > expiresAtMs. Call heartbeat() periodically to keep the entry alive. value() always filters out expired entries before returning.
Server restart behavior
Presence is ephemeral by design — entries are stored in sled but the TTL clock is wall-time based. When the server restarts:
- All presence entries that were already expired remain expired.
- Entries that were still live will appear missing for connected clients until clients re-heartbeat.
- Clients receive a fresh empty state on reconnect, then repopulate as they send heartbeats.
This is expected behavior. To handle it gracefully, send a heartbeat() immediately after the WebSocket connects (or reconnects):
const room = client.presence("pr:room-123");
client.waitForConnected().then(() => {
room.heartbeat({ status: "online" }, 30_000);
});
// And refresh periodically
setInterval(() => room.heartbeat({ status: "online" }, 30_000), 10_000);
There is no server-side “grace period” after restart. If your use case requires durable presence (e.g., last-seen timestamps), use a LwwRegister instead — it persists across restarts.