or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

channel.mdindex.mdpresence.mdpush.mdsocket.mdutilities.md

presence.mddocs/

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

```