CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-types--phoenix

TypeScript definitions for Phoenix JavaScript client library enabling real-time WebSocket communication with Phoenix Framework applications

Overview
Eval results
Files

presence.mddocs/

Presence Tracking

Real-time presence tracking system for monitoring user join/leave events and maintaining synchronized presence state across connected clients.

Capabilities

Presence Constructor

Creates a new Presence instance for tracking user presence on a channel.

/**
 * Creates a new Presence instance for tracking user presence on a channel
 * @param channel - Channel to track presence on
 * @param opts - Presence configuration options
 */
constructor(channel: Channel, opts?: PresenceOpts);

interface PresenceOpts {
  /** Custom event names for presence state and diff events */
  events?: { state: string; diff: string } | undefined;
}

Usage Example:

import { Presence, Channel, Socket } from "phoenix";

const socket = new Socket("/socket");
const channel = socket.channel("room:lobby");

// Basic presence tracking
const presence = new Presence(channel);

// Custom presence event names
const customPresence = new Presence(channel, {
  events: {
    state: "custom_presence_state",
    diff: "custom_presence_diff"
  }
});

Presence Event Handlers

Register callbacks for presence join, leave, and sync events.

/**
 * Register callback for user join events
 * @param callback - Function to call when users join
 */
onJoin(callback: PresenceOnJoinCallback): void;

/**
 * Register callback for user leave events
 * @param callback - Function to call when users leave
 */
onLeave(callback: PresenceOnLeaveCallback): void;

/**
 * Register callback for presence state sync events
 * @param callback - Function to call when presence state is synchronized
 */
onSync(callback: () => void | Promise<void>): void;

type PresenceOnJoinCallback = (key?: string, currentPresence?: any, newPresence?: any) => void;
type PresenceOnLeaveCallback = (key?: string, currentPresence?: any, newPresence?: any) => void;

Usage Example:

// Handle user joins
presence.onJoin((id, current, newPresence) => {
  if (current) {
    console.log(`User ${id} presence updated:`, newPresence);
  } else {
    console.log(`User ${id} joined:`, newPresence);
  }

  // Update UI
  updateUserList();
  showNotification(`${newPresence.metas[0].username} is now online`);
});

// Handle user leaves
presence.onLeave((id, current, leftPresence) => {
  if (current.metas.length === 0) {
    console.log(`User ${id} left entirely:`, leftPresence);
    showNotification(`${leftPresence.metas[0].username} went offline`);
  } else {
    console.log(`User ${id} left from some devices:`, leftPresence);
  }

  updateUserList();
});

// Handle presence sync
presence.onSync(() => {
  console.log("Presence state synchronized");

  // Update the entire user list
  const users = presence.list((id, presence) => ({
    id,
    username: presence.metas[0].username,
    online_at: presence.metas[0].online_at,
    device_count: presence.metas.length
  }));

  renderUserList(users);
});

Presence State Management

List current presences and check sync state.

/**
 * List current presences with optional transformation
 * @param chooser - Optional function to transform each presence entry
 * @returns Array of presence entries (transformed if chooser provided)
 */
list<T = any>(chooser?: (key: string, presence: any) => T): T[];

/**
 * Check if presence is in pending sync state
 * @returns True if sync is pending, false otherwise
 */
inPendingSyncState(): boolean;

Usage Example:

// Get all users (raw presence data)
const allPresences = presence.list();
console.log("All presences:", allPresences);

// Transform presence data
const users = presence.list((userId, presence) => ({
  id: userId,
  name: presence.metas[0].username,
  status: presence.metas[0].status,
  joinedAt: presence.metas[0].online_at,
  deviceCount: presence.metas.length,
  devices: presence.metas.map(meta => meta.device_type)
}));

// Check if we're waiting for sync
if (presence.inPendingSyncState()) {
  console.log("Waiting for presence synchronization...");
  showLoadingIndicator();
} else {
  console.log(`${users.length} users currently online`);
  hideLoadingIndicator();
}

Static Presence Methods

Utility methods for manually managing presence state and diffs.

/**
 * Synchronize presence state manually
 * @param currentState - Current presence state object
 * @param newState - New presence state object
 * @param onJoin - Optional callback for join events
 * @param onLeave - Optional callback for leave events
 * @returns Updated presence state
 */
static syncState(
  currentState: object,
  newState: object,
  onJoin?: PresenceOnJoinCallback,
  onLeave?: PresenceOnLeaveCallback
): any;

/**
 * Apply presence diff to current state
 * @param currentState - Current presence state object
 * @param diff - Presence diff with joins and leaves
 * @param onJoin - Optional callback for join events
 * @param onLeave - Optional callback for leave events
 * @returns Updated presence state
 */
static syncDiff(
  currentState: object,
  diff: { joins: object; leaves: object },
  onJoin?: PresenceOnJoinCallback,
  onLeave?: PresenceOnLeaveCallback
): any;

/**
 * List presences from a state object
 * @param presences - Presence state object
 * @param chooser - Optional transformation function
 * @returns Array of presence entries
 */
static list<T = any>(presences: object, chooser?: (key: string, presence: any) => T): T[];

Usage Example:

// Manual presence state management
let presenceState = {};

// Handle Phoenix presence_state events manually
channel.on("presence_state", (state) => {
  const onJoin = (id: string, current: any, newPresence: any) => {
    console.log("Manual join:", id, newPresence);
  };

  const onLeave = (id: string, current: any, leftPresence: any) => {
    console.log("Manual leave:", id, leftPresence);
  };

  presenceState = Presence.syncState(presenceState, state, onJoin, onLeave);
  renderPresenceList();
});

// Handle Phoenix presence_diff events manually
channel.on("presence_diff", (diff) => {
  presenceState = Presence.syncDiff(presenceState, diff);
  renderPresenceList();
});

function renderPresenceList() {
  const users = Presence.list(presenceState, (id, presence) => ({
    id,
    name: presence.metas[0].username
  }));

  console.log("Current users:", users);
}

Advanced Usage Patterns

User Status Tracking

interface UserPresence {
  id: string;
  username: string;
  status: 'online' | 'away' | 'busy' | 'offline';
  lastSeen: Date;
  deviceCount: number;
  devices: string[];
}

class UserPresenceTracker {
  private users: Map<string, UserPresence> = new Map();
  private callbacks: Array<(users: UserPresence[]) => void> = [];

  constructor(private presence: Presence) {
    this.setupPresenceHandlers();
  }

  private setupPresenceHandlers() {
    this.presence.onJoin((id, current, newPresence) => {
      const user: UserPresence = {
        id,
        username: newPresence.metas[0].username,
        status: newPresence.metas[0].status || 'online',
        lastSeen: new Date(newPresence.metas[0].online_at),
        deviceCount: newPresence.metas.length,
        devices: newPresence.metas.map((meta: any) => meta.device_type)
      };

      this.users.set(id, user);
      this.notifyCallbacks();
    });

    this.presence.onLeave((id, current, leftPresence) => {
      if (current.metas.length === 0) {
        // User completely offline
        const user = this.users.get(id);
        if (user) {
          user.status = 'offline';
          user.lastSeen = new Date();
          user.deviceCount = 0;
          user.devices = [];
        }
      } else {
        // User still has some devices online
        const user = this.users.get(id);
        if (user) {
          user.deviceCount = current.metas.length;
          user.devices = current.metas.map((meta: any) => meta.device_type);
        }
      }

      this.notifyCallbacks();
    });

    this.presence.onSync(() => {
      this.syncAllUsers();
    });
  }

  private syncAllUsers() {
    this.users.clear();

    this.presence.list((id, presence) => {
      const user: UserPresence = {
        id,
        username: presence.metas[0].username,
        status: presence.metas[0].status || 'online',
        lastSeen: new Date(presence.metas[0].online_at),
        deviceCount: presence.metas.length,
        devices: presence.metas.map((meta: any) => meta.device_type)
      };

      this.users.set(id, user);
    });

    this.notifyCallbacks();
  }

  private notifyCallbacks() {
    const userList = Array.from(this.users.values());
    this.callbacks.forEach(callback => callback(userList));
  }

  onUsersChanged(callback: (users: UserPresence[]) => void) {
    this.callbacks.push(callback);
    // Call immediately with current users
    callback(Array.from(this.users.values()));
  }

  getUser(id: string): UserPresence | undefined {
    return this.users.get(id);
  }

  getAllUsers(): UserPresence[] {
    return Array.from(this.users.values());
  }

  getOnlineUsers(): UserPresence[] {
    return this.getAllUsers().filter(user => user.status !== 'offline');
  }

  getUserCount(): number {
    return this.users.size;
  }
}

// Usage
const userTracker = new UserPresenceTracker(presence);

userTracker.onUsersChanged((users) => {
  console.log(`${users.length} users tracked`);
  console.log(`${userTracker.getOnlineUsers().length} users online`);

  // Update UI
  renderUserList(users);
});

Typing Indicators

class TypingIndicator {
  private typingUsers: Set<string> = new Set();
  private typingTimeouts: Map<string, NodeJS.Timeout> = new Map();
  private callbacks: Array<(users: string[]) => void> = [];

  constructor(private channel: Channel) {
    this.setupTypingHandlers();
  }

  private setupTypingHandlers() {
    this.channel.on("user_typing", ({ user_id, username }) => {
      this.typingUsers.add(username);

      // Clear existing timeout
      const existingTimeout = this.typingTimeouts.get(user_id);
      if (existingTimeout) {
        clearTimeout(existingTimeout);
      }

      // Set new timeout (user stops typing after 3 seconds)
      const timeout = setTimeout(() => {
        this.typingUsers.delete(username);
        this.typingTimeouts.delete(user_id);
        this.notifyCallbacks();
      }, 3000);

      this.typingTimeouts.set(user_id, timeout);
      this.notifyCallbacks();
    });

    this.channel.on("user_stopped_typing", ({ username }) => {
      this.typingUsers.delete(username);
      this.notifyCallbacks();
    });
  }

  startTyping(userId: string) {
    this.channel.push("typing_start", { user_id: userId });
  }

  stopTyping(userId: string) {
    this.channel.push("typing_stop", { user_id: userId });
  }

  private notifyCallbacks() {
    const typingList = Array.from(this.typingUsers);
    this.callbacks.forEach(callback => callback(typingList));
  }

  onTypingChanged(callback: (users: string[]) => void) {
    this.callbacks.push(callback);
  }

  getTypingUsers(): string[] {
    return Array.from(this.typingUsers);
  }
}

// Usage
const typingIndicator = new TypingIndicator(channel);

typingIndicator.onTypingChanged((users) => {
  const indicator = document.getElementById("typing-indicator");
  if (users.length > 0) {
    indicator.textContent = `${users.join(", ")} ${users.length === 1 ? 'is' : 'are'} typing...`;
    indicator.style.display = "block";
  } else {
    indicator.style.display = "none";
  }
});

// Start typing when user types in input
const messageInput = document.getElementById("message-input") as HTMLInputElement;
let typingTimer: NodeJS.Timeout;

messageInput.addEventListener("input", () => {
  typingIndicator.startTyping("current_user_id");

  // Stop typing after 1 second of inactivity
  clearTimeout(typingTimer);
  typingTimer = setTimeout(() => {
    typingIndicator.stopTyping("current_user_id");
  }, 1000);
});

Activity Monitoring

class ActivityMonitor {
  private lastActivity: Map<string, Date> = new Map();
  private activityCheckInterval: NodeJS.Timeout;

  constructor(private presence: Presence, private checkIntervalMs = 30000) {
    this.setupActivityMonitoring();
  }

  private setupActivityMonitoring() {
    // Track user activity from presence
    this.presence.onJoin((id, current, newPresence) => {
      this.lastActivity.set(id, new Date());
    });

    // Periodically check for inactive users
    this.activityCheckInterval = setInterval(() => {
      this.checkInactiveUsers();
    }, this.checkIntervalMs);
  }

  private checkInactiveUsers() {
    const now = new Date();
    const inactiveThreshold = 5 * 60 * 1000; // 5 minutes

    this.presence.list((id, presence) => {
      const lastSeen = this.lastActivity.get(id) || new Date(presence.metas[0].online_at);
      const timeSinceActivity = now.getTime() - lastSeen.getTime();

      if (timeSinceActivity > inactiveThreshold) {
        console.log(`User ${id} has been inactive for ${Math.round(timeSinceActivity / 60000)} minutes`);
        this.markUserInactive(id);
      }
    });
  }

  private markUserInactive(userId: string) {
    // Emit inactive user event or update UI
    document.dispatchEvent(new CustomEvent("user-inactive", {
      detail: { userId }
    }));
  }

  updateUserActivity(userId: string) {
    this.lastActivity.set(userId, new Date());
  }

  destroy() {
    if (this.activityCheckInterval) {
      clearInterval(this.activityCheckInterval);
    }
  }
}

// Usage
const activityMonitor = new ActivityMonitor(presence);

// Update activity on user actions
document.addEventListener("click", () => {
  activityMonitor.updateUserActivity("current_user_id");
});

document.addEventListener("keypress", () => {
  activityMonitor.updateUserActivity("current_user_id");
});

Install with Tessl CLI

npx tessl i tessl/npm-types--phoenix

docs

channel.md

index.md

presence.md

push.md

socket.md

utilities.md

tile.json