0
# Presence Tracking
1
2
Real-time presence tracking system for monitoring user join/leave events and maintaining synchronized presence state across connected clients.
3
4
## Capabilities
5
6
### Presence Constructor
7
8
Creates a new Presence instance for tracking user presence on a channel.
9
10
```typescript { .api }
11
/**
12
* Creates a new Presence instance for tracking user presence on a channel
13
* @param channel - Channel to track presence on
14
* @param opts - Presence configuration options
15
*/
16
constructor(channel: Channel, opts?: PresenceOpts);
17
18
interface PresenceOpts {
19
/** Custom event names for presence state and diff events */
20
events?: { state: string; diff: string } | undefined;
21
}
22
```
23
24
**Usage Example:**
25
26
```typescript
27
import { Presence, Channel, Socket } from "phoenix";
28
29
const socket = new Socket("/socket");
30
const channel = socket.channel("room:lobby");
31
32
// Basic presence tracking
33
const presence = new Presence(channel);
34
35
// Custom presence event names
36
const customPresence = new Presence(channel, {
37
events: {
38
state: "custom_presence_state",
39
diff: "custom_presence_diff"
40
}
41
});
42
```
43
44
### Presence Event Handlers
45
46
Register callbacks for presence join, leave, and sync events.
47
48
```typescript { .api }
49
/**
50
* Register callback for user join events
51
* @param callback - Function to call when users join
52
*/
53
onJoin(callback: PresenceOnJoinCallback): void;
54
55
/**
56
* Register callback for user leave events
57
* @param callback - Function to call when users leave
58
*/
59
onLeave(callback: PresenceOnLeaveCallback): void;
60
61
/**
62
* Register callback for presence state sync events
63
* @param callback - Function to call when presence state is synchronized
64
*/
65
onSync(callback: () => void | Promise<void>): void;
66
67
type PresenceOnJoinCallback = (key?: string, currentPresence?: any, newPresence?: any) => void;
68
type PresenceOnLeaveCallback = (key?: string, currentPresence?: any, newPresence?: any) => void;
69
```
70
71
**Usage Example:**
72
73
```typescript
74
// Handle user joins
75
presence.onJoin((id, current, newPresence) => {
76
if (current) {
77
console.log(`User ${id} presence updated:`, newPresence);
78
} else {
79
console.log(`User ${id} joined:`, newPresence);
80
}
81
82
// Update UI
83
updateUserList();
84
showNotification(`${newPresence.metas[0].username} is now online`);
85
});
86
87
// Handle user leaves
88
presence.onLeave((id, current, leftPresence) => {
89
if (current.metas.length === 0) {
90
console.log(`User ${id} left entirely:`, leftPresence);
91
showNotification(`${leftPresence.metas[0].username} went offline`);
92
} else {
93
console.log(`User ${id} left from some devices:`, leftPresence);
94
}
95
96
updateUserList();
97
});
98
99
// Handle presence sync
100
presence.onSync(() => {
101
console.log("Presence state synchronized");
102
103
// Update the entire user list
104
const users = presence.list((id, presence) => ({
105
id,
106
username: presence.metas[0].username,
107
online_at: presence.metas[0].online_at,
108
device_count: presence.metas.length
109
}));
110
111
renderUserList(users);
112
});
113
```
114
115
### Presence State Management
116
117
List current presences and check sync state.
118
119
```typescript { .api }
120
/**
121
* List current presences with optional transformation
122
* @param chooser - Optional function to transform each presence entry
123
* @returns Array of presence entries (transformed if chooser provided)
124
*/
125
list<T = any>(chooser?: (key: string, presence: any) => T): T[];
126
127
/**
128
* Check if presence is in pending sync state
129
* @returns True if sync is pending, false otherwise
130
*/
131
inPendingSyncState(): boolean;
132
```
133
134
**Usage Example:**
135
136
```typescript
137
// Get all users (raw presence data)
138
const allPresences = presence.list();
139
console.log("All presences:", allPresences);
140
141
// Transform presence data
142
const users = presence.list((userId, presence) => ({
143
id: userId,
144
name: presence.metas[0].username,
145
status: presence.metas[0].status,
146
joinedAt: presence.metas[0].online_at,
147
deviceCount: presence.metas.length,
148
devices: presence.metas.map(meta => meta.device_type)
149
}));
150
151
// Check if we're waiting for sync
152
if (presence.inPendingSyncState()) {
153
console.log("Waiting for presence synchronization...");
154
showLoadingIndicator();
155
} else {
156
console.log(`${users.length} users currently online`);
157
hideLoadingIndicator();
158
}
159
```
160
161
### Static Presence Methods
162
163
Utility methods for manually managing presence state and diffs.
164
165
```typescript { .api }
166
/**
167
* Synchronize presence state manually
168
* @param currentState - Current presence state object
169
* @param newState - New presence state object
170
* @param onJoin - Optional callback for join events
171
* @param onLeave - Optional callback for leave events
172
* @returns Updated presence state
173
*/
174
static syncState(
175
currentState: object,
176
newState: object,
177
onJoin?: PresenceOnJoinCallback,
178
onLeave?: PresenceOnLeaveCallback
179
): any;
180
181
/**
182
* Apply presence diff to current state
183
* @param currentState - Current presence state object
184
* @param diff - Presence diff with joins and leaves
185
* @param onJoin - Optional callback for join events
186
* @param onLeave - Optional callback for leave events
187
* @returns Updated presence state
188
*/
189
static syncDiff(
190
currentState: object,
191
diff: { joins: object; leaves: object },
192
onJoin?: PresenceOnJoinCallback,
193
onLeave?: PresenceOnLeaveCallback
194
): any;
195
196
/**
197
* List presences from a state object
198
* @param presences - Presence state object
199
* @param chooser - Optional transformation function
200
* @returns Array of presence entries
201
*/
202
static list<T = any>(presences: object, chooser?: (key: string, presence: any) => T): T[];
203
```
204
205
**Usage Example:**
206
207
```typescript
208
// Manual presence state management
209
let presenceState = {};
210
211
// Handle Phoenix presence_state events manually
212
channel.on("presence_state", (state) => {
213
const onJoin = (id: string, current: any, newPresence: any) => {
214
console.log("Manual join:", id, newPresence);
215
};
216
217
const onLeave = (id: string, current: any, leftPresence: any) => {
218
console.log("Manual leave:", id, leftPresence);
219
};
220
221
presenceState = Presence.syncState(presenceState, state, onJoin, onLeave);
222
renderPresenceList();
223
});
224
225
// Handle Phoenix presence_diff events manually
226
channel.on("presence_diff", (diff) => {
227
presenceState = Presence.syncDiff(presenceState, diff);
228
renderPresenceList();
229
});
230
231
function renderPresenceList() {
232
const users = Presence.list(presenceState, (id, presence) => ({
233
id,
234
name: presence.metas[0].username
235
}));
236
237
console.log("Current users:", users);
238
}
239
```
240
241
## Advanced Usage Patterns
242
243
### User Status Tracking
244
245
```typescript
246
interface UserPresence {
247
id: string;
248
username: string;
249
status: 'online' | 'away' | 'busy' | 'offline';
250
lastSeen: Date;
251
deviceCount: number;
252
devices: string[];
253
}
254
255
class UserPresenceTracker {
256
private users: Map<string, UserPresence> = new Map();
257
private callbacks: Array<(users: UserPresence[]) => void> = [];
258
259
constructor(private presence: Presence) {
260
this.setupPresenceHandlers();
261
}
262
263
private setupPresenceHandlers() {
264
this.presence.onJoin((id, current, newPresence) => {
265
const user: UserPresence = {
266
id,
267
username: newPresence.metas[0].username,
268
status: newPresence.metas[0].status || 'online',
269
lastSeen: new Date(newPresence.metas[0].online_at),
270
deviceCount: newPresence.metas.length,
271
devices: newPresence.metas.map((meta: any) => meta.device_type)
272
};
273
274
this.users.set(id, user);
275
this.notifyCallbacks();
276
});
277
278
this.presence.onLeave((id, current, leftPresence) => {
279
if (current.metas.length === 0) {
280
// User completely offline
281
const user = this.users.get(id);
282
if (user) {
283
user.status = 'offline';
284
user.lastSeen = new Date();
285
user.deviceCount = 0;
286
user.devices = [];
287
}
288
} else {
289
// User still has some devices online
290
const user = this.users.get(id);
291
if (user) {
292
user.deviceCount = current.metas.length;
293
user.devices = current.metas.map((meta: any) => meta.device_type);
294
}
295
}
296
297
this.notifyCallbacks();
298
});
299
300
this.presence.onSync(() => {
301
this.syncAllUsers();
302
});
303
}
304
305
private syncAllUsers() {
306
this.users.clear();
307
308
this.presence.list((id, presence) => {
309
const user: UserPresence = {
310
id,
311
username: presence.metas[0].username,
312
status: presence.metas[0].status || 'online',
313
lastSeen: new Date(presence.metas[0].online_at),
314
deviceCount: presence.metas.length,
315
devices: presence.metas.map((meta: any) => meta.device_type)
316
};
317
318
this.users.set(id, user);
319
});
320
321
this.notifyCallbacks();
322
}
323
324
private notifyCallbacks() {
325
const userList = Array.from(this.users.values());
326
this.callbacks.forEach(callback => callback(userList));
327
}
328
329
onUsersChanged(callback: (users: UserPresence[]) => void) {
330
this.callbacks.push(callback);
331
// Call immediately with current users
332
callback(Array.from(this.users.values()));
333
}
334
335
getUser(id: string): UserPresence | undefined {
336
return this.users.get(id);
337
}
338
339
getAllUsers(): UserPresence[] {
340
return Array.from(this.users.values());
341
}
342
343
getOnlineUsers(): UserPresence[] {
344
return this.getAllUsers().filter(user => user.status !== 'offline');
345
}
346
347
getUserCount(): number {
348
return this.users.size;
349
}
350
}
351
352
// Usage
353
const userTracker = new UserPresenceTracker(presence);
354
355
userTracker.onUsersChanged((users) => {
356
console.log(`${users.length} users tracked`);
357
console.log(`${userTracker.getOnlineUsers().length} users online`);
358
359
// Update UI
360
renderUserList(users);
361
});
362
```
363
364
### Typing Indicators
365
366
```typescript
367
class TypingIndicator {
368
private typingUsers: Set<string> = new Set();
369
private typingTimeouts: Map<string, NodeJS.Timeout> = new Map();
370
private callbacks: Array<(users: string[]) => void> = [];
371
372
constructor(private channel: Channel) {
373
this.setupTypingHandlers();
374
}
375
376
private setupTypingHandlers() {
377
this.channel.on("user_typing", ({ user_id, username }) => {
378
this.typingUsers.add(username);
379
380
// Clear existing timeout
381
const existingTimeout = this.typingTimeouts.get(user_id);
382
if (existingTimeout) {
383
clearTimeout(existingTimeout);
384
}
385
386
// Set new timeout (user stops typing after 3 seconds)
387
const timeout = setTimeout(() => {
388
this.typingUsers.delete(username);
389
this.typingTimeouts.delete(user_id);
390
this.notifyCallbacks();
391
}, 3000);
392
393
this.typingTimeouts.set(user_id, timeout);
394
this.notifyCallbacks();
395
});
396
397
this.channel.on("user_stopped_typing", ({ username }) => {
398
this.typingUsers.delete(username);
399
this.notifyCallbacks();
400
});
401
}
402
403
startTyping(userId: string) {
404
this.channel.push("typing_start", { user_id: userId });
405
}
406
407
stopTyping(userId: string) {
408
this.channel.push("typing_stop", { user_id: userId });
409
}
410
411
private notifyCallbacks() {
412
const typingList = Array.from(this.typingUsers);
413
this.callbacks.forEach(callback => callback(typingList));
414
}
415
416
onTypingChanged(callback: (users: string[]) => void) {
417
this.callbacks.push(callback);
418
}
419
420
getTypingUsers(): string[] {
421
return Array.from(this.typingUsers);
422
}
423
}
424
425
// Usage
426
const typingIndicator = new TypingIndicator(channel);
427
428
typingIndicator.onTypingChanged((users) => {
429
const indicator = document.getElementById("typing-indicator");
430
if (users.length > 0) {
431
indicator.textContent = `${users.join(", ")} ${users.length === 1 ? 'is' : 'are'} typing...`;
432
indicator.style.display = "block";
433
} else {
434
indicator.style.display = "none";
435
}
436
});
437
438
// Start typing when user types in input
439
const messageInput = document.getElementById("message-input") as HTMLInputElement;
440
let typingTimer: NodeJS.Timeout;
441
442
messageInput.addEventListener("input", () => {
443
typingIndicator.startTyping("current_user_id");
444
445
// Stop typing after 1 second of inactivity
446
clearTimeout(typingTimer);
447
typingTimer = setTimeout(() => {
448
typingIndicator.stopTyping("current_user_id");
449
}, 1000);
450
});
451
```
452
453
### Activity Monitoring
454
455
```typescript
456
class ActivityMonitor {
457
private lastActivity: Map<string, Date> = new Map();
458
private activityCheckInterval: NodeJS.Timeout;
459
460
constructor(private presence: Presence, private checkIntervalMs = 30000) {
461
this.setupActivityMonitoring();
462
}
463
464
private setupActivityMonitoring() {
465
// Track user activity from presence
466
this.presence.onJoin((id, current, newPresence) => {
467
this.lastActivity.set(id, new Date());
468
});
469
470
// Periodically check for inactive users
471
this.activityCheckInterval = setInterval(() => {
472
this.checkInactiveUsers();
473
}, this.checkIntervalMs);
474
}
475
476
private checkInactiveUsers() {
477
const now = new Date();
478
const inactiveThreshold = 5 * 60 * 1000; // 5 minutes
479
480
this.presence.list((id, presence) => {
481
const lastSeen = this.lastActivity.get(id) || new Date(presence.metas[0].online_at);
482
const timeSinceActivity = now.getTime() - lastSeen.getTime();
483
484
if (timeSinceActivity > inactiveThreshold) {
485
console.log(`User ${id} has been inactive for ${Math.round(timeSinceActivity / 60000)} minutes`);
486
this.markUserInactive(id);
487
}
488
});
489
}
490
491
private markUserInactive(userId: string) {
492
// Emit inactive user event or update UI
493
document.dispatchEvent(new CustomEvent("user-inactive", {
494
detail: { userId }
495
}));
496
}
497
498
updateUserActivity(userId: string) {
499
this.lastActivity.set(userId, new Date());
500
}
501
502
destroy() {
503
if (this.activityCheckInterval) {
504
clearInterval(this.activityCheckInterval);
505
}
506
}
507
}
508
509
// Usage
510
const activityMonitor = new ActivityMonitor(presence);
511
512
// Update activity on user actions
513
document.addEventListener("click", () => {
514
activityMonitor.updateUserActivity("current_user_id");
515
});
516
517
document.addEventListener("keypress", () => {
518
activityMonitor.updateUserActivity("current_user_id");
519
});
520
```