Presence (Unified API)
Most apps don’t care about connections — they care about users. But by default, Mesh tracks presence per connection.
If a user opens two tabs, each tab has a different connection ID. That’s intentional — but it means presence data can look messy unless you group it.
The createPresence(...)
helper handles this for you.
It:
- Deduplicates presence by user ID (or any logic you define)
- Syncs your own presence state via
localStorage
- Automatically joins the room and tracks updates
- Calls your
onUpdate
handler when the user list changes
If the connection drops, the presence helper resets itself and restores your state automatically after reconnecting.
Quick example
import { MeshClient } from "@mesh-kit/core/client";
import { createPresence } from "@mesh-kit/core/client-utils";
const client = new MeshClient("ws://localhost:3000");
await client.connect();
// generate or reuse a persistent ID (defined below) to identify the user by
const userId = getOrCreateId("demo-userId");
const presence = createPresence({
client,
room: "general",
storageKey: "user-state:general",
stateIdentifier: (state) => state?.userId,
onUpdate: (users) => {
// users: Array<{ id, state, tabCount }> where 'id' is the value returned by stateIdentifier
render(users);
},
});
// broadcast your initial state
await presence.publish({
userId,
username: "Alice",
status: "online",
});
What it does
createPresence(...)
combines two things:
-
Deduplicated presence tracking
Groups connections using astateIdentifier(state)
function (e.g. by userId) -
Presence state publishing + sync
Stores your own presence state inlocalStorage
and republish it after reconnects -
Auto recovery on reconnect
If the connection drops, presence is re-initialized on reconnect and your last known state is republished
API
const presence = createPresence({
client, // required — MeshClient
room, // required — string
storageKey, // required — key used to persist your state in localStorage
stateIdentifier, // required — function (sync or async) that returns a group ID from a state
onUpdate, // required — (users: Array<{ id, state, tabCount }>) => void
});
The returned object has:
publish(state)
— sets and broadcasts your presence stateread()
— reads your last state fromlocalStorage
dispose()
— cleans up event listeners and leaves the room
Complete browser demo
<!DOCTYPE html>
<html>
<head><title>Presence Demo</title></head>
<body>
<h2>Users (<span id="count">0</span>)</h2>
<input id="username" placeholder="Username" />
<input id="status" placeholder="Status" />
<button id="update">Update</button>
<button id="clear">Clear</button>
<ul id="user-list"></ul>
<script type="module">
import { MeshClient } from "https://esm.sh/@mesh-kit/core/client";
import { createPresence } from "https://esm.sh/@mesh-kit/core/client-utils";
const client = new MeshClient("ws://localhost:3000");
await client.connect();
const userId = getOrCreateId("demo-userId");
const render = (users) => {
document.getElementById("count").textContent = users.length;
const ul = document.getElementById("user-list");
ul.innerHTML = "";
for (const user of users) {
const li = document.createElement("li");
li.textContent = `${user.id}: ${user.state?.status ?? "idle"} (tabs: ${user.tabCount})`;
ul.appendChild(li);
}
};
const presence = createPresence({
client,
room: "general",
storageKey: "user-state:general",
stateIdentifier: (state) => state?.userId,
onUpdate: render,
});
const cached = presence.read();
const usernameInput = document.getElementById("username");
const statusInput = document.getElementById("status");
usernameInput.value = cached?.username || "";
statusInput.value = cached?.status || "";
const getPresenceData = () => ({
userId,
username: usernameInput.value,
status: statusInput.value,
});
// publish initial presence
await presence.publish(getPresenceData());
document.getElementById("update").addEventListener("click", async () => {
await presence.publish(getPresenceData());
});
document.getElementById("clear").addEventListener("click", async () => {
usernameInput.value = "";
statusInput.value = "";
await presence.publish(getPresenceData());
});
function getOrCreateId(key) {
let id = localStorage.getItem(key);
if (!id) {
id = "user-" + Math.random().toString(36).slice(2, 8);
localStorage.setItem(key, id);
}
return id;
}
</script>
</body>
</html>