Skip to Content
Client SDKUnified Presence

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:

  1. Deduplicated presence tracking
    Groups connections using a stateIdentifier(state) function (e.g. by userId)

  2. Presence state publishing + sync
    Stores your own presence state in localStorage and republish it after reconnects

  3. 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 state
  • read() — reads your last state from localStorage
  • 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>
Last updated on
© 2025