SDK ReferenceCore API

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 on
  • handler: (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