@principal-ai/control-tower-core 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +116 -0
  2. package/dist/abstractions/DefaultPresenceManager.d.ts +40 -0
  3. package/dist/abstractions/DefaultPresenceManager.d.ts.map +1 -0
  4. package/dist/abstractions/DefaultPresenceManager.js +256 -0
  5. package/dist/abstractions/PresenceManager.d.ts +127 -0
  6. package/dist/abstractions/PresenceManager.d.ts.map +1 -0
  7. package/dist/abstractions/PresenceManager.js +80 -0
  8. package/dist/abstractions/index.d.ts +2 -0
  9. package/dist/abstractions/index.d.ts.map +1 -1
  10. package/dist/abstractions/index.js +5 -1
  11. package/dist/adapters/mock/MockTransportAdapter.d.ts +37 -1
  12. package/dist/adapters/mock/MockTransportAdapter.d.ts.map +1 -1
  13. package/dist/adapters/mock/MockTransportAdapter.js +101 -2
  14. package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts.map +1 -1
  15. package/dist/adapters/websocket/WebSocketTransportAdapter.js +5 -3
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +23 -19
  19. package/dist/index.js.map +11 -7
  20. package/dist/index.mjs +688 -31
  21. package/dist/index.mjs.map +11 -7
  22. package/dist/server/BaseServer.d.ts +72 -1
  23. package/dist/server/BaseServer.d.ts.map +1 -1
  24. package/dist/server/BaseServer.js +234 -28
  25. package/dist/server/ExperimentalAPI.d.ts +137 -0
  26. package/dist/server/ExperimentalAPI.d.ts.map +1 -0
  27. package/dist/server/ExperimentalAPI.js +239 -0
  28. package/dist/server/ServerBuilder.d.ts +22 -1
  29. package/dist/server/ServerBuilder.d.ts.map +1 -1
  30. package/dist/server/ServerBuilder.js +31 -1
  31. package/dist/server/index.d.ts +1 -0
  32. package/dist/server/index.d.ts.map +1 -1
  33. package/dist/server/index.js +3 -1
  34. package/dist/types/experimental.d.ts +136 -0
  35. package/dist/types/experimental.d.ts.map +1 -0
  36. package/dist/types/experimental.js +32 -0
  37. package/dist/types/index.d.ts +2 -0
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js +3 -0
  40. package/dist/types/presence.d.ts +163 -0
  41. package/dist/types/presence.d.ts.map +1 -0
  42. package/dist/types/presence.js +8 -0
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -36,6 +36,7 @@ console.log('Server started on port 8080');
36
36
  - **Real-time Events**: Event broadcasting and history
37
37
  - **Extensible**: Abstract interfaces for custom implementations
38
38
  - **TypeScript**: Full type safety with generated declarations
39
+ - **Experimental APIs**: Opt-in broadcast features for rapid prototyping (see [Experimental Features](#experimental-features))
39
40
 
40
41
  ## API Reference
41
42
 
@@ -79,6 +80,121 @@ class CustomRoomManager extends RoomManager {
79
80
  }
80
81
  ```
81
82
 
83
+ ## Experimental Features
84
+
85
+ ⚠️ **For Development Use Only**
86
+
87
+ Control Tower Core provides experimental broadcast APIs for rapid prototyping and internal tooling. These APIs allow broadcasting messages beyond room boundaries.
88
+
89
+ ### Enabling Experimental Features
90
+
91
+ ```typescript
92
+ import { ServerBuilder } from '@principal-ai/control-tower-core';
93
+
94
+ const server = new ServerBuilder()
95
+ .withTransport(transportAdapter)
96
+ .withRoomManager(roomManager)
97
+ .withLockManager(lockManager)
98
+ .withExperimentalFeatures({
99
+ enableBroadcast: true // Enable experimental broadcast APIs
100
+ })
101
+ .build();
102
+ ```
103
+
104
+ ### Using Experimental Broadcast
105
+
106
+ ```typescript
107
+ // Broadcast to all connected clients
108
+ await server.experimental.broadcast({
109
+ type: 'system:announcement',
110
+ data: { message: 'Server maintenance in 5 minutes' }
111
+ });
112
+
113
+ // Broadcast to authenticated users only
114
+ await server.experimental.broadcastAuthenticated({
115
+ type: 'presence:user_online',
116
+ data: { userId: 'alice' }
117
+ });
118
+
119
+ // Broadcast with custom filtering
120
+ await server.experimental.broadcastWhere(
121
+ (client) => client.userId.startsWith('admin_'),
122
+ { type: 'admin:alert', data: { ... } }
123
+ );
124
+
125
+ // Send to specific users
126
+ await server.experimental.sendToUsers(
127
+ ['user1', 'user2'],
128
+ { type: 'notification:mention', data: { ... } }
129
+ );
130
+ ```
131
+
132
+ ### Important Notes
133
+
134
+ - **Experimental APIs may change or be removed** without major version bumps
135
+ - **Not recommended for production** applications
136
+ - Use for prototyping, then submit a [feature request](docs/FEATURE_REQUEST_TEMPLATE.md) to graduate to stable API
137
+ - See [full documentation](docs/EXPERIMENTAL_BROADCAST.md) for details
138
+
139
+ ## Testing
140
+
141
+ Control Tower Core provides comprehensive test utilities for building reliable applications.
142
+
143
+ ### MockTransportAdapter
144
+
145
+ The `MockTransportAdapter` provides test helpers for simulating client-server interactions:
146
+
147
+ ```typescript
148
+ import { MockTransportAdapter } from '@principal-ai/control-tower-core';
149
+
150
+ const transport = new MockTransportAdapter();
151
+
152
+ // Simulate a client connection
153
+ await transport.simulateConnection('client-id', {
154
+ authenticated: true,
155
+ userId: 'user-123',
156
+ metadata: { deviceType: 'desktop' }
157
+ });
158
+
159
+ // Simulate client messages
160
+ await transport.simulateClientMessage('client-id', 'join_room', {
161
+ roomId: 'room-1'
162
+ });
163
+
164
+ // Simulate incoming server messages (for client tests)
165
+ transport.simulateIncomingMessage({
166
+ type: 'room_joined',
167
+ payload: { roomId: 'room-1', state: { ... } }
168
+ });
169
+
170
+ // Track connection attempts (useful for reconnection tests)
171
+ const attempts = transport.getConnectionAttempts();
172
+ transport.resetConnectionAttempts();
173
+
174
+ // Get messages sent to specific clients
175
+ const messages = transport.getSentMessages('client-id');
176
+ ```
177
+
178
+ ### Running Tests
179
+
180
+ ```bash
181
+ # Run all tests
182
+ bun test
183
+
184
+ # Run specific test file
185
+ bun test tests/BaseServer.test.ts
186
+
187
+ # Run with coverage
188
+ bun test --coverage
189
+ ```
190
+
191
+ ## Documentation
192
+
193
+ - [Experimental Broadcast APIs](docs/EXPERIMENTAL_BROADCAST.md)
194
+ - [Feature Request Template](docs/FEATURE_REQUEST_TEMPLATE.md)
195
+ - [Architecture Design](DESIGN.md)
196
+ - [Changelog](CHANGELOG.md)
197
+
82
198
  ## License
83
199
 
84
200
  MIT
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Default In-Memory Presence Manager
3
+ *
4
+ * Reference implementation of PresenceManager using in-memory storage.
5
+ * Suitable for single-server deployments and development.
6
+ *
7
+ * For multi-server deployments, implement a custom PresenceManager
8
+ * backed by Redis or another shared data store.
9
+ */
10
+ import { PresenceManager } from './PresenceManager.js';
11
+ import type { UserPresence, DeviceInfo, PresenceStatus, PresenceConfig, PresenceChangeEvent, ActivityUpdate } from '../types/presence.js';
12
+ export declare class DefaultPresenceManager extends PresenceManager {
13
+ private userPresences;
14
+ private deviceToUser;
15
+ private gracePeriodEntries;
16
+ constructor(config?: PresenceConfig);
17
+ connectDevice(userId: string, deviceId: string, deviceInfo?: Partial<DeviceInfo>): Promise<UserPresence>;
18
+ disconnectDevice(userId: string, deviceId: string): Promise<UserPresence | null>;
19
+ updateActivity(update: ActivityUpdate): Promise<UserPresence>;
20
+ getUserPresence(userId: string): Promise<UserPresence | null>;
21
+ getUsersPresence(userIds: string[]): Promise<Map<string, UserPresence>>;
22
+ getOnlineUsers(): Promise<UserPresence[]>;
23
+ getUserDevices(userId: string): Promise<DeviceInfo[]>;
24
+ setUserStatus(userId: string, status: PresenceStatus): Promise<UserPresence>;
25
+ processHeartbeats(): Promise<PresenceChangeEvent[]>;
26
+ clear(): Promise<void>;
27
+ /**
28
+ * Get count of users in grace period (useful for monitoring)
29
+ */
30
+ getGracePeriodCount(): number;
31
+ /**
32
+ * Get total connected device count
33
+ */
34
+ getConnectedDeviceCount(): number;
35
+ /**
36
+ * Get total unique user count
37
+ */
38
+ getUniqueUserCount(): number;
39
+ }
40
+ //# sourceMappingURL=DefaultPresenceManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DefaultPresenceManager.d.ts","sourceRoot":"","sources":["../../src/abstractions/DefaultPresenceManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAW9B,qBAAa,sBAAuB,SAAQ,eAAe;IACzD,OAAO,CAAC,aAAa,CAAmC;IACxD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,kBAAkB,CAAuC;gBAErD,MAAM,CAAC,EAAE,cAAc;IAI7B,aAAa,CACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC,YAAY,CAAC;IA+DlB,gBAAgB,CACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA6CzB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IA4B7D,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAI7D,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAavE,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAYzC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IASrD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAU5E,iBAAiB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IA4EnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;OAEG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;OAEG;IACH,uBAAuB,IAAI,MAAM;IAIjC;;OAEG;IACH,kBAAkB,IAAI,MAAM;CAG7B"}
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ /**
3
+ * Default In-Memory Presence Manager
4
+ *
5
+ * Reference implementation of PresenceManager using in-memory storage.
6
+ * Suitable for single-server deployments and development.
7
+ *
8
+ * For multi-server deployments, implement a custom PresenceManager
9
+ * backed by Redis or another shared data store.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.DefaultPresenceManager = void 0;
13
+ const PresenceManager_js_1 = require("./PresenceManager.js");
14
+ class DefaultPresenceManager extends PresenceManager_js_1.PresenceManager {
15
+ constructor(config) {
16
+ super(config);
17
+ this.userPresences = new Map();
18
+ this.deviceToUser = new Map();
19
+ this.gracePeriodEntries = new Map();
20
+ }
21
+ async connectDevice(userId, deviceId, deviceInfo) {
22
+ const now = Date.now();
23
+ // Check if user is in grace period (reconnecting)
24
+ const gracePeriodEntry = this.gracePeriodEntries.get(userId);
25
+ if (gracePeriodEntry) {
26
+ // Restore from grace period
27
+ this.gracePeriodEntries.delete(userId);
28
+ const presence = gracePeriodEntry.previousPresence;
29
+ // Add the new device
30
+ const device = {
31
+ deviceId,
32
+ type: deviceInfo?.type,
33
+ connectedAt: now,
34
+ lastActivity: now,
35
+ metadata: deviceInfo?.metadata
36
+ };
37
+ presence.devices.set(deviceId, device);
38
+ presence.status = 'online';
39
+ presence.lastActivity = now;
40
+ this.userPresences.set(userId, presence);
41
+ this.deviceToUser.set(deviceId, userId);
42
+ return presence;
43
+ }
44
+ // Get or create user presence
45
+ let presence = this.userPresences.get(userId);
46
+ if (!presence) {
47
+ // New user
48
+ presence = {
49
+ userId,
50
+ status: 'online',
51
+ devices: new Map(),
52
+ firstConnectedAt: now,
53
+ lastActivity: now,
54
+ metadata: {}
55
+ };
56
+ this.userPresences.set(userId, presence);
57
+ }
58
+ // Add device
59
+ const device = {
60
+ deviceId,
61
+ type: deviceInfo?.type,
62
+ connectedAt: now,
63
+ lastActivity: now,
64
+ metadata: deviceInfo?.metadata
65
+ };
66
+ presence.devices.set(deviceId, device);
67
+ presence.status = this.calculatePresenceStatus(presence.devices, now);
68
+ presence.lastActivity = now;
69
+ this.deviceToUser.set(deviceId, userId);
70
+ return presence;
71
+ }
72
+ async disconnectDevice(userId, deviceId) {
73
+ const presence = this.userPresences.get(userId);
74
+ if (!presence) {
75
+ return null;
76
+ }
77
+ // Remove device
78
+ presence.devices.delete(deviceId);
79
+ this.deviceToUser.delete(deviceId);
80
+ const now = Date.now();
81
+ // If no devices left, start grace period
82
+ if (presence.devices.size === 0) {
83
+ if (this.config.gracePeriod > 0) {
84
+ // Move to grace period
85
+ this.gracePeriodEntries.set(userId, {
86
+ userId,
87
+ disconnectedAt: now,
88
+ previousPresence: presence
89
+ });
90
+ this.userPresences.delete(userId);
91
+ return null;
92
+ }
93
+ else {
94
+ // No grace period, remove completely
95
+ this.userPresences.delete(userId);
96
+ return null;
97
+ }
98
+ }
99
+ // Still has devices, update status
100
+ presence.status = this.calculatePresenceStatus(presence.devices, now);
101
+ // Update lastActivity to most recent device activity
102
+ let mostRecentActivity = 0;
103
+ for (const device of presence.devices.values()) {
104
+ if (device.lastActivity > mostRecentActivity) {
105
+ mostRecentActivity = device.lastActivity;
106
+ }
107
+ }
108
+ presence.lastActivity = mostRecentActivity;
109
+ return presence;
110
+ }
111
+ async updateActivity(update) {
112
+ const { userId, deviceId, timestamp } = update;
113
+ const presence = this.userPresences.get(userId);
114
+ if (!presence) {
115
+ throw new Error(`User ${userId} not found in presence system`);
116
+ }
117
+ const device = presence.devices.get(deviceId);
118
+ if (!device) {
119
+ throw new Error(`Device ${deviceId} not found for user ${userId}`);
120
+ }
121
+ // Update device activity
122
+ device.lastActivity = timestamp;
123
+ // Update user activity
124
+ if (timestamp > presence.lastActivity) {
125
+ presence.lastActivity = timestamp;
126
+ }
127
+ // Recalculate status
128
+ const previousStatus = presence.status;
129
+ presence.status = this.calculatePresenceStatus(presence.devices, timestamp);
130
+ return presence;
131
+ }
132
+ async getUserPresence(userId) {
133
+ return this.userPresences.get(userId) || null;
134
+ }
135
+ async getUsersPresence(userIds) {
136
+ const result = new Map();
137
+ for (const userId of userIds) {
138
+ const presence = this.userPresences.get(userId);
139
+ if (presence) {
140
+ result.set(userId, presence);
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+ async getOnlineUsers() {
146
+ const online = [];
147
+ for (const presence of this.userPresences.values()) {
148
+ if (presence.status === 'online') {
149
+ online.push(presence);
150
+ }
151
+ }
152
+ return online;
153
+ }
154
+ async getUserDevices(userId) {
155
+ const presence = this.userPresences.get(userId);
156
+ if (!presence) {
157
+ return [];
158
+ }
159
+ return Array.from(presence.devices.values());
160
+ }
161
+ async setUserStatus(userId, status) {
162
+ const presence = this.userPresences.get(userId);
163
+ if (!presence) {
164
+ throw new Error(`User ${userId} not found in presence system`);
165
+ }
166
+ presence.status = status;
167
+ return presence;
168
+ }
169
+ async processHeartbeats() {
170
+ const now = Date.now();
171
+ const changes = [];
172
+ // Process active presences
173
+ for (const [userId, presence] of this.userPresences.entries()) {
174
+ const devicesToRemove = [];
175
+ // Check each device for timeout
176
+ for (const [deviceId, device] of presence.devices.entries()) {
177
+ if (this.shouldDisconnectDevice(device, now)) {
178
+ devicesToRemove.push(deviceId);
179
+ }
180
+ }
181
+ // Remove timed-out devices
182
+ for (const deviceId of devicesToRemove) {
183
+ presence.devices.delete(deviceId);
184
+ this.deviceToUser.delete(deviceId);
185
+ }
186
+ // Update status after removing devices
187
+ const previousStatus = presence.status;
188
+ const newStatus = this.calculatePresenceStatus(presence.devices, now);
189
+ if (newStatus !== previousStatus) {
190
+ presence.status = newStatus;
191
+ changes.push({
192
+ userId,
193
+ previousStatus,
194
+ status: newStatus,
195
+ timestamp: now,
196
+ reason: 'heartbeat_timeout'
197
+ });
198
+ }
199
+ // If no devices left, move to grace period
200
+ if (presence.devices.size === 0) {
201
+ if (this.config.gracePeriod > 0) {
202
+ this.gracePeriodEntries.set(userId, {
203
+ userId,
204
+ disconnectedAt: now,
205
+ previousPresence: presence
206
+ });
207
+ }
208
+ this.userPresences.delete(userId);
209
+ }
210
+ }
211
+ // Process grace period expirations
212
+ const expiredGracePeriods = [];
213
+ for (const [userId, entry] of this.gracePeriodEntries.entries()) {
214
+ const gracePeriodElapsed = now - entry.disconnectedAt;
215
+ if (gracePeriodElapsed > this.config.gracePeriod) {
216
+ expiredGracePeriods.push(userId);
217
+ changes.push({
218
+ userId,
219
+ previousStatus: entry.previousPresence.status,
220
+ status: 'offline',
221
+ timestamp: now,
222
+ reason: 'grace_period_expired'
223
+ });
224
+ }
225
+ }
226
+ // Remove expired grace period entries
227
+ for (const userId of expiredGracePeriods) {
228
+ this.gracePeriodEntries.delete(userId);
229
+ }
230
+ return changes;
231
+ }
232
+ async clear() {
233
+ this.userPresences.clear();
234
+ this.deviceToUser.clear();
235
+ this.gracePeriodEntries.clear();
236
+ }
237
+ /**
238
+ * Get count of users in grace period (useful for monitoring)
239
+ */
240
+ getGracePeriodCount() {
241
+ return this.gracePeriodEntries.size;
242
+ }
243
+ /**
244
+ * Get total connected device count
245
+ */
246
+ getConnectedDeviceCount() {
247
+ return this.deviceToUser.size;
248
+ }
249
+ /**
250
+ * Get total unique user count
251
+ */
252
+ getUniqueUserCount() {
253
+ return this.userPresences.size + this.gracePeriodEntries.size;
254
+ }
255
+ }
256
+ exports.DefaultPresenceManager = DefaultPresenceManager;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Presence Manager Abstract Class
3
+ *
4
+ * Manages global user presence tracking independent of room membership.
5
+ * Supports multi-device connections, activity monitoring, and grace periods.
6
+ */
7
+ import type { UserPresence, DeviceInfo, PresenceStatus, PresenceConfig, PresenceChangeEvent, ActivityUpdate } from '../types/presence.js';
8
+ /**
9
+ * Abstract class for managing user presence
10
+ *
11
+ * Implementations should handle:
12
+ * - Multi-device tracking per user
13
+ * - Activity/heartbeat monitoring
14
+ * - Grace period for disconnects
15
+ * - Presence state persistence (if needed)
16
+ */
17
+ export declare abstract class PresenceManager {
18
+ protected config: Required<PresenceConfig>;
19
+ constructor(config?: PresenceConfig);
20
+ /**
21
+ * Register a new device connection for a user
22
+ *
23
+ * @param userId - User identifier
24
+ * @param deviceId - Device/client identifier
25
+ * @param deviceInfo - Optional device metadata
26
+ * @returns The updated user presence
27
+ */
28
+ abstract connectDevice(userId: string, deviceId: string, deviceInfo?: Partial<DeviceInfo>): Promise<UserPresence>;
29
+ /**
30
+ * Unregister a device connection
31
+ *
32
+ * May trigger grace period if this is the user's last device.
33
+ *
34
+ * @param userId - User identifier
35
+ * @param deviceId - Device/client identifier
36
+ * @returns The updated user presence, or null if user is completely offline
37
+ */
38
+ abstract disconnectDevice(userId: string, deviceId: string): Promise<UserPresence | null>;
39
+ /**
40
+ * Update activity timestamp for a device
41
+ *
42
+ * Used for heartbeats and activity tracking.
43
+ *
44
+ * @param update - Activity update information
45
+ * @returns The updated user presence
46
+ */
47
+ abstract updateActivity(update: ActivityUpdate): Promise<UserPresence>;
48
+ /**
49
+ * Get presence state for a user
50
+ *
51
+ * @param userId - User identifier
52
+ * @returns User presence, or null if not found
53
+ */
54
+ abstract getUserPresence(userId: string): Promise<UserPresence | null>;
55
+ /**
56
+ * Get presence state for multiple users
57
+ *
58
+ * @param userIds - Array of user identifiers
59
+ * @returns Map of userId to UserPresence
60
+ */
61
+ abstract getUsersPresence(userIds: string[]): Promise<Map<string, UserPresence>>;
62
+ /**
63
+ * Get all online users
64
+ *
65
+ * @returns Array of user presences with status 'online'
66
+ */
67
+ abstract getOnlineUsers(): Promise<UserPresence[]>;
68
+ /**
69
+ * Get all devices for a user
70
+ *
71
+ * @param userId - User identifier
72
+ * @returns Array of device info
73
+ */
74
+ abstract getUserDevices(userId: string): Promise<DeviceInfo[]>;
75
+ /**
76
+ * Manually set user status
77
+ *
78
+ * @param userId - User identifier
79
+ * @param status - New presence status
80
+ * @returns The updated user presence
81
+ */
82
+ abstract setUserStatus(userId: string, status: PresenceStatus): Promise<UserPresence>;
83
+ /**
84
+ * Check for stale connections and update presence accordingly
85
+ *
86
+ * This should be called periodically to:
87
+ * - Mark idle devices as 'away'
88
+ * - Disconnect devices that haven't sent heartbeats
89
+ * - Expire grace periods
90
+ *
91
+ * @returns Array of presence change events
92
+ */
93
+ abstract processHeartbeats(): Promise<PresenceChangeEvent[]>;
94
+ /**
95
+ * Clear all presence data
96
+ *
97
+ * Useful for testing and cleanup.
98
+ */
99
+ abstract clear(): Promise<void>;
100
+ /**
101
+ * Get configuration
102
+ */
103
+ getConfig(): Readonly<Required<PresenceConfig>>;
104
+ /**
105
+ * Check if presence tracking is enabled
106
+ */
107
+ isEnabled(): boolean;
108
+ /**
109
+ * Calculate presence status based on device states
110
+ *
111
+ * Helper method for implementations to determine overall user status.
112
+ *
113
+ * @param devices - Map of devices for a user
114
+ * @param now - Current timestamp
115
+ * @returns Calculated presence status
116
+ */
117
+ protected calculatePresenceStatus(devices: Map<string, DeviceInfo>, now?: number): PresenceStatus;
118
+ /**
119
+ * Check if a device should be disconnected due to missed heartbeats
120
+ *
121
+ * @param device - Device info
122
+ * @param now - Current timestamp
123
+ * @returns true if device should be disconnected
124
+ */
125
+ protected shouldDisconnectDevice(device: DeviceInfo, now?: number): boolean;
126
+ }
127
+ //# sourceMappingURL=PresenceManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PresenceManager.d.ts","sourceRoot":"","sources":["../../src/abstractions/PresenceManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,cAAc,EACf,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;GAQG;AACH,8BAAsB,eAAe;IACnC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;gBAE/B,MAAM,CAAC,EAAE,cAAc;IAanC;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC,YAAY,CAAC;IAExB;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CACvB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAE/B;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAEtE;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAEtE;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAEhF;;;;OAIG;IACH,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAElD;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAE9D;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;IAErF;;;;;;;;;OASG;IACH,QAAQ,CAAC,iBAAiB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAE5D;;;;OAIG;IACH,QAAQ,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAI/C;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;;;;;OAQG;IACH,SAAS,CAAC,uBAAuB,CAC/B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAChC,GAAG,GAAE,MAAmB,GACvB,cAAc;IAmBjB;;;;;;OAMG;IACH,SAAS,CAAC,sBAAsB,CAC9B,MAAM,EAAE,UAAU,EAClB,GAAG,GAAE,MAAmB,GACvB,OAAO;CAIX"}
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /**
3
+ * Presence Manager Abstract Class
4
+ *
5
+ * Manages global user presence tracking independent of room membership.
6
+ * Supports multi-device connections, activity monitoring, and grace periods.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.PresenceManager = void 0;
10
+ /**
11
+ * Abstract class for managing user presence
12
+ *
13
+ * Implementations should handle:
14
+ * - Multi-device tracking per user
15
+ * - Activity/heartbeat monitoring
16
+ * - Grace period for disconnects
17
+ * - Presence state persistence (if needed)
18
+ */
19
+ class PresenceManager {
20
+ constructor(config) {
21
+ // Set defaults
22
+ this.config = {
23
+ enabled: config?.enabled ?? false,
24
+ heartbeatInterval: config?.heartbeatInterval ?? 30000,
25
+ awayThreshold: config?.awayThreshold ?? 2,
26
+ disconnectThreshold: config?.disconnectThreshold ?? 3,
27
+ gracePeriod: config?.gracePeriod ?? 30000,
28
+ trackDeviceActivity: config?.trackDeviceActivity ?? true,
29
+ broadcastPresenceUpdates: config?.broadcastPresenceUpdates ?? true
30
+ };
31
+ }
32
+ /**
33
+ * Get configuration
34
+ */
35
+ getConfig() {
36
+ return { ...this.config };
37
+ }
38
+ /**
39
+ * Check if presence tracking is enabled
40
+ */
41
+ isEnabled() {
42
+ return this.config.enabled;
43
+ }
44
+ /**
45
+ * Calculate presence status based on device states
46
+ *
47
+ * Helper method for implementations to determine overall user status.
48
+ *
49
+ * @param devices - Map of devices for a user
50
+ * @param now - Current timestamp
51
+ * @returns Calculated presence status
52
+ */
53
+ calculatePresenceStatus(devices, now = Date.now()) {
54
+ if (devices.size === 0) {
55
+ return 'offline';
56
+ }
57
+ const heartbeatTimeout = this.config.heartbeatInterval * this.config.awayThreshold;
58
+ // Check if ANY device is active (within heartbeat threshold)
59
+ for (const device of devices.values()) {
60
+ const timeSinceActivity = now - device.lastActivity;
61
+ if (timeSinceActivity < heartbeatTimeout) {
62
+ return 'online';
63
+ }
64
+ }
65
+ // All devices are idle
66
+ return 'away';
67
+ }
68
+ /**
69
+ * Check if a device should be disconnected due to missed heartbeats
70
+ *
71
+ * @param device - Device info
72
+ * @param now - Current timestamp
73
+ * @returns true if device should be disconnected
74
+ */
75
+ shouldDisconnectDevice(device, now = Date.now()) {
76
+ const timeout = this.config.heartbeatInterval * this.config.disconnectThreshold;
77
+ return (now - device.lastActivity) > timeout;
78
+ }
79
+ }
80
+ exports.PresenceManager = PresenceManager;
@@ -6,4 +6,6 @@ export { RoomManager } from './RoomManager.js';
6
6
  export { LockManager } from './LockManager.js';
7
7
  export { DefaultRoomManager } from './DefaultRoomManager.js';
8
8
  export { DefaultLockManager } from './DefaultLockManager.js';
9
+ export { PresenceManager } from './PresenceManager.js';
10
+ export { DefaultPresenceManager } from './DefaultPresenceManager.js';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/abstractions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/abstractions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DefaultLockManager = exports.DefaultRoomManager = exports.LockManager = exports.RoomManager = exports.TypedEventEmitter = void 0;
3
+ exports.DefaultPresenceManager = exports.PresenceManager = exports.DefaultLockManager = exports.DefaultRoomManager = exports.LockManager = exports.RoomManager = exports.TypedEventEmitter = void 0;
4
4
  var EventEmitter_js_1 = require("./EventEmitter.js");
5
5
  Object.defineProperty(exports, "TypedEventEmitter", { enumerable: true, get: function () { return EventEmitter_js_1.TypedEventEmitter; } });
6
6
  var RoomManager_js_1 = require("./RoomManager.js");
@@ -11,3 +11,7 @@ var DefaultRoomManager_js_1 = require("./DefaultRoomManager.js");
11
11
  Object.defineProperty(exports, "DefaultRoomManager", { enumerable: true, get: function () { return DefaultRoomManager_js_1.DefaultRoomManager; } });
12
12
  var DefaultLockManager_js_1 = require("./DefaultLockManager.js");
13
13
  Object.defineProperty(exports, "DefaultLockManager", { enumerable: true, get: function () { return DefaultLockManager_js_1.DefaultLockManager; } });
14
+ var PresenceManager_js_1 = require("./PresenceManager.js");
15
+ Object.defineProperty(exports, "PresenceManager", { enumerable: true, get: function () { return PresenceManager_js_1.PresenceManager; } });
16
+ var DefaultPresenceManager_js_1 = require("./DefaultPresenceManager.js");
17
+ Object.defineProperty(exports, "DefaultPresenceManager", { enumerable: true, get: function () { return DefaultPresenceManager_js_1.DefaultPresenceManager; } });