SDK ReferenceCore
Presence Tracking
Real-time presence tracking and user awareness APIs
Presence Tracking
Real-time presence tracking allows you to monitor user activity and build awareness features like "who's online", active user lists, and typing indicators.
Core Presence APIs
onPresence(topic, handler): Promise<void>
Register a presence handler for online/offline events.
Parameters:
topic: string
- Topic to track presence onhandler: (presence: PresencePacket) => void
- Presence callback
Presence Format:
interface PresencePacket {
clientId: string; // Client that changed status
topic: string; // Topic name
status: "online" | "offline"; // Presence status
timestamp: number; // Event timestamp
subscribers?: string[]; // Optional: list of current subscribers
}
Example:
await client.onPresence("chat:lobby", (presence) => {
console.log(`${presence.clientId} is ${presence.status}`);
updateUserList(presence.clientId, presence.status);
});
offPresence(topic, handler): void
Remove a specific presence handler.
const handler = (p) => console.log(p);
await client.onPresence("chat:lobby", handler);
// Later...
client.offPresence("chat:lobby", handler);
clearPresenceHandlers(topic): void
Remove all presence handlers for a topic.
client.clearPresenceHandlers("chat:lobby");
Presence Events
Presence events are automatically triggered when:
- A client subscribes to a topic (online)
- A client unsubscribes from a topic (offline)
- A client's connection is lost (offline)
- A client reconnects (online)
await client.onPresence("chat:room-123", (presence) => {
switch (presence.status) {
case "online":
console.log(`👤 ${presence.clientId} joined the room`);
break;
case "offline":
console.log(`👋 ${presence.clientId} left the room`);
break;
}
});
Advanced Presence Patterns
User List Management
class UserPresenceManager {
private onlineUsers = new Set<string>();
private userInfo = new Map<string, { name: string; avatar?: string }>();
constructor(topic: string) {
client.onPresence(topic, (presence) => {
this.handlePresence(presence);
});
}
private handlePresence(presence: PresencePacket) {
if (presence.status === "online") {
this.onlineUsers.add(presence.clientId);
this.onUserJoin(presence.clientId);
} else {
this.onlineUsers.delete(presence.clientId);
this.onUserLeave(presence.clientId);
}
this.updateUI();
}
private onUserJoin(clientId: string) {
// Fetch user info if not cached
if (!this.userInfo.has(clientId)) {
this.fetchUserInfo(clientId);
}
}
private onUserLeave(clientId: string) {
// Optional: Clean up user info after delay
setTimeout(() => {
if (!this.onlineUsers.has(clientId)) {
this.userInfo.delete(clientId);
}
}, 60000); // 1 minute
}
private updateUI() {
const count = this.onlineUsers.size;
document.getElementById("user-count")!.textContent = `${count} online`;
// Update user list
const userList = document.getElementById("user-list")!;
userList.innerHTML = "";
for (const clientId of this.onlineUsers) {
const userInfo = this.userInfo.get(clientId);
const userElement = document.createElement("div");
userElement.textContent = userInfo?.name || clientId;
userList.appendChild(userElement);
}
}
getOnlineUsers(): string[] {
return Array.from(this.onlineUsers);
}
isUserOnline(clientId: string): boolean {
return this.onlineUsers.has(clientId);
}
private async fetchUserInfo(clientId: string) {
// Implement user info fetching logic
// This would typically call your API
}
}
Typing Indicators
class TypingIndicator {
private typingUsers = new Map<string, NodeJS.Timeout>();
private typingTimeoutMs = 3000; // 3 seconds
constructor(private topic: string) {
// Subscribe to typing events
client.subscribe(`${topic}:typing`, (msg) => {
this.handleTypingEvent(msg.senderId, msg.payload === "start");
});
}
startTyping() {
// Broadcast typing start
client.publish(`${this.topic}:typing`, "start");
// Auto-stop typing after timeout
setTimeout(() => {
this.stopTyping();
}, this.typingTimeoutMs);
}
stopTyping() {
// Broadcast typing stop
client.publish(`${this.topic}:typing`, "stop");
}
private handleTypingEvent(clientId: string, isTyping: boolean) {
if (isTyping) {
// Clear existing timeout
const existingTimeout = this.typingUsers.get(clientId);
if (existingTimeout) {
clearTimeout(existingTimeout);
}
// Set new timeout
const timeout = setTimeout(() => {
this.typingUsers.delete(clientId);
this.updateTypingIndicator();
}, this.typingTimeoutMs);
this.typingUsers.set(clientId, timeout);
} else {
// Remove user from typing list
const timeout = this.typingUsers.get(clientId);
if (timeout) {
clearTimeout(timeout);
this.typingUsers.delete(clientId);
}
}
this.updateTypingIndicator();
}
private updateTypingIndicator() {
const typingList = Array.from(this.typingUsers.keys());
const indicator = document.getElementById("typing-indicator")!;
if (typingList.length === 0) {
indicator.textContent = "";
} else if (typingList.length === 1) {
indicator.textContent = `${typingList[0]} is typing...`;
} else if (typingList.length === 2) {
indicator.textContent = `${typingList[0]} and ${typingList[1]} are typing...`;
} else {
indicator.textContent = `${typingList.length} people are typing...`;
}
}
getTypingUsers(): string[] {
return Array.from(this.typingUsers.keys());
}
}
Active Status Tracking
class ActivityTracker {
private lastActivity = new Map<string, number>();
private activityThreshold = 5 * 60 * 1000; // 5 minutes
constructor(topic: string) {
client.onPresence(topic, (presence) => {
this.updateActivity(presence.clientId, presence.status === "online");
});
// Subscribe to activity pings
client.subscribe(`${topic}:activity`, (msg) => {
this.updateActivity(msg.senderId, true);
});
// Periodically check for inactive users
setInterval(() => this.checkInactivity(), 60000); // Every minute
}
private updateActivity(clientId: string, isActive: boolean) {
if (isActive) {
this.lastActivity.set(clientId, Date.now());
} else {
this.lastActivity.delete(clientId);
}
this.updateActivityStatus();
}
private checkInactivity() {
const now = Date.now();
const inactiveUsers: string[] = [];
for (const [clientId, lastSeen] of this.lastActivity) {
if (now - lastSeen > this.activityThreshold) {
inactiveUsers.push(clientId);
}
}
// Mark inactive users
for (const clientId of inactiveUsers) {
this.updateActivity(clientId, false);
}
}
private updateActivityStatus() {
const now = Date.now();
const activeUsers: string[] = [];
const idleUsers: string[] = [];
for (const [clientId, lastSeen] of this.lastActivity) {
const timeSinceActivity = now - lastSeen;
if (timeSinceActivity < 60000) { // Less than 1 minute
activeUsers.push(clientId);
} else if (timeSinceActivity < this.activityThreshold) {
idleUsers.push(clientId);
}
}
this.renderActivityStatus(activeUsers, idleUsers);
}
private renderActivityStatus(active: string[], idle: string[]) {
// Update UI to show active/idle status
// Green dot for active, yellow for idle, gray for offline
}
sendActivityPing() {
// Call this on user interactions (mouse move, keypress, etc.)
client.publish(`${this.topic}:activity`, "ping");
}
}
Multi-Topic Presence
class MultiTopicPresence {
private presenceHandlers = new Map<string, Set<(presence: PresencePacket) => void>>();
private topicUsers = new Map<string, Set<string>>();
async trackPresence(topic: string, handler: (presence: PresencePacket) => void) {
// Add handler to topic
if (!this.presenceHandlers.has(topic)) {
this.presenceHandlers.set(topic, new Set());
this.topicUsers.set(topic, new Set());
// Set up presence tracking for this topic
await client.onPresence(topic, (presence) => {
this.handlePresence(topic, presence);
});
}
this.presenceHandlers.get(topic)!.add(handler);
}
private handlePresence(topic: string, presence: PresencePacket) {
const users = this.topicUsers.get(topic)!;
if (presence.status === "online") {
users.add(presence.clientId);
} else {
users.delete(presence.clientId);
}
// Call all handlers for this topic
const handlers = this.presenceHandlers.get(topic)!;
for (const handler of handlers) {
try {
handler(presence);
} catch (error) {
console.error("Error in presence handler:", error);
}
}
}
getTopicUsers(topic: string): string[] {
return Array.from(this.topicUsers.get(topic) || []);
}
getUserTopics(clientId: string): string[] {
const topics: string[] = [];
for (const [topic, users] of this.topicUsers) {
if (users.has(clientId)) {
topics.push(topic);
}
}
return topics;
}
getTotalActiveUsers(): number {
const allUsers = new Set<string>();
for (const users of this.topicUsers.values()) {
for (const user of users) {
allUsers.add(user);
}
}
return allUsers.size;
}
}
UI Integration Examples
React Hook for Presence
import { useEffect, useState } from 'react';
interface PresenceState {
onlineUsers: string[];
userCount: number;
isLoading: boolean;
}
export function usePresence(topic: string): PresenceState {
const [state, setState] = useState<PresenceState>({
onlineUsers: [],
userCount: 0,
isLoading: true
});
useEffect(() => {
const onlineUsers = new Set<string>();
const handlePresence = (presence: PresencePacket) => {
if (presence.status === "online") {
onlineUsers.add(presence.clientId);
} else {
onlineUsers.delete(presence.clientId);
}
setState({
onlineUsers: Array.from(onlineUsers),
userCount: onlineUsers.size,
isLoading: false
});
};
client.onPresence(topic, handlePresence);
return () => {
client.offPresence(topic, handlePresence);
};
}, [topic]);
return state;
}
Vue Composition for Presence
import { ref, onMounted, onUnmounted } from 'vue';
export function usePresence(topic: string) {
const onlineUsers = ref<string[]>([]);
const userCount = ref(0);
const isLoading = ref(true);
let presenceHandler: (presence: PresencePacket) => void;
onMounted(async () => {
const users = new Set<string>();
presenceHandler = (presence) => {
if (presence.status === "online") {
users.add(presence.clientId);
} else {
users.delete(presence.clientId);
}
onlineUsers.value = Array.from(users);
userCount.value = users.size;
isLoading.value = false;
};
await client.onPresence(topic, presenceHandler);
});
onUnmounted(() => {
if (presenceHandler) {
client.offPresence(topic, presenceHandler);
}
});
return {
onlineUsers,
userCount,
isLoading
};
}
Best Practices
1. Handle Presence Gracefully
await client.onPresence("chat:room", (presence) => {
// Always check if user exists before updating UI
const userElement = document.getElementById(`user-${presence.clientId}`);
if (!userElement && presence.status === "offline") {
// User wasn't in UI anyway, ignore
return;
}
updateUserPresence(presence.clientId, presence.status);
});
2. Batch Presence Updates
class PresenceBatcher {
private pendingUpdates = new Map<string, PresencePacket>();
private batchTimeout?: NodeJS.Timeout;
handlePresence(presence: PresencePacket) {
this.pendingUpdates.set(presence.clientId, presence);
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => this.flush(), 100);
}
}
private flush() {
const updates = Array.from(this.pendingUpdates.values());
this.pendingUpdates.clear();
this.batchTimeout = undefined;
// Apply all updates at once
this.applyBatchedUpdates(updates);
}
private applyBatchedUpdates(updates: PresencePacket[]) {
// Update UI with all presence changes at once
}
}
3. Memory Management
class PresenceManager {
private presenceTimers = new Map<string, NodeJS.Timeout>();
handlePresence(presence: PresencePacket) {
if (presence.status === "offline") {
// Set timer to clean up user data
const timer = setTimeout(() => {
this.cleanupUser(presence.clientId);
}, 60000); // Clean up after 1 minute
this.presenceTimers.set(presence.clientId, timer);
} else {
// Cancel cleanup timer if user comes back online
const timer = this.presenceTimers.get(presence.clientId);
if (timer) {
clearTimeout(timer);
this.presenceTimers.delete(presence.clientId);
}
}
}
private cleanupUser(clientId: string) {
// Remove user from UI and free memory
this.presenceTimers.delete(clientId);
}
}
Next Steps
- Messaging APIs - Publish and subscribe patterns
- Connection Management - Connection lifecycle and states
- Types Reference - Complete TypeScript definitions
- Getting Started - Basic API walkthrough