TypeScript definitions for Phoenix JavaScript client library enabling real-time WebSocket communication with Phoenix Framework applications
Real-time presence tracking system for monitoring user join/leave events and maintaining synchronized presence state across connected clients.
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"
}
});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);
});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();
}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);
}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);
});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);
});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