@principal-ai/control-tower-core 0.1.7 → 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.
- package/README.md +53 -0
- package/dist/abstractions/DefaultPresenceManager.d.ts +40 -0
- package/dist/abstractions/DefaultPresenceManager.d.ts.map +1 -0
- package/dist/abstractions/DefaultPresenceManager.js +256 -0
- package/dist/abstractions/PresenceManager.d.ts +127 -0
- package/dist/abstractions/PresenceManager.d.ts.map +1 -0
- package/dist/abstractions/PresenceManager.js +80 -0
- package/dist/abstractions/index.d.ts +2 -0
- package/dist/abstractions/index.d.ts.map +1 -1
- package/dist/abstractions/index.js +5 -1
- package/dist/adapters/mock/MockTransportAdapter.d.ts +37 -1
- package/dist/adapters/mock/MockTransportAdapter.d.ts.map +1 -1
- package/dist/adapters/mock/MockTransportAdapter.js +101 -2
- package/dist/adapters/websocket/WebSocketTransportAdapter.d.ts.map +1 -1
- package/dist/adapters/websocket/WebSocketTransportAdapter.js +5 -3
- package/dist/index.js.map +9 -7
- package/dist/index.mjs +511 -30
- package/dist/index.mjs.map +9 -7
- package/dist/server/BaseServer.d.ts +59 -0
- package/dist/server/BaseServer.d.ts.map +1 -1
- package/dist/server/BaseServer.js +219 -28
- package/dist/server/ServerBuilder.d.ts +11 -0
- package/dist/server/ServerBuilder.d.ts.map +1 -1
- package/dist/server/ServerBuilder.js +13 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/presence.d.ts +163 -0
- package/dist/types/presence.d.ts.map +1 -0
- package/dist/types/presence.js +8 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3170,6 +3170,255 @@ class DefaultLockManager extends LockManager {
|
|
|
3170
3170
|
}
|
|
3171
3171
|
}
|
|
3172
3172
|
}
|
|
3173
|
+
// src/abstractions/PresenceManager.ts
|
|
3174
|
+
class PresenceManager {
|
|
3175
|
+
config;
|
|
3176
|
+
constructor(config) {
|
|
3177
|
+
this.config = {
|
|
3178
|
+
enabled: config?.enabled ?? false,
|
|
3179
|
+
heartbeatInterval: config?.heartbeatInterval ?? 30000,
|
|
3180
|
+
awayThreshold: config?.awayThreshold ?? 2,
|
|
3181
|
+
disconnectThreshold: config?.disconnectThreshold ?? 3,
|
|
3182
|
+
gracePeriod: config?.gracePeriod ?? 30000,
|
|
3183
|
+
trackDeviceActivity: config?.trackDeviceActivity ?? true,
|
|
3184
|
+
broadcastPresenceUpdates: config?.broadcastPresenceUpdates ?? true
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
getConfig() {
|
|
3188
|
+
return { ...this.config };
|
|
3189
|
+
}
|
|
3190
|
+
isEnabled() {
|
|
3191
|
+
return this.config.enabled;
|
|
3192
|
+
}
|
|
3193
|
+
calculatePresenceStatus(devices, now = Date.now()) {
|
|
3194
|
+
if (devices.size === 0) {
|
|
3195
|
+
return "offline";
|
|
3196
|
+
}
|
|
3197
|
+
const heartbeatTimeout = this.config.heartbeatInterval * this.config.awayThreshold;
|
|
3198
|
+
for (const device of devices.values()) {
|
|
3199
|
+
const timeSinceActivity = now - device.lastActivity;
|
|
3200
|
+
if (timeSinceActivity < heartbeatTimeout) {
|
|
3201
|
+
return "online";
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
return "away";
|
|
3205
|
+
}
|
|
3206
|
+
shouldDisconnectDevice(device, now = Date.now()) {
|
|
3207
|
+
const timeout = this.config.heartbeatInterval * this.config.disconnectThreshold;
|
|
3208
|
+
return now - device.lastActivity > timeout;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
// src/abstractions/DefaultPresenceManager.ts
|
|
3212
|
+
class DefaultPresenceManager extends PresenceManager {
|
|
3213
|
+
userPresences = new Map;
|
|
3214
|
+
deviceToUser = new Map;
|
|
3215
|
+
gracePeriodEntries = new Map;
|
|
3216
|
+
constructor(config) {
|
|
3217
|
+
super(config);
|
|
3218
|
+
}
|
|
3219
|
+
async connectDevice(userId, deviceId, deviceInfo) {
|
|
3220
|
+
const now = Date.now();
|
|
3221
|
+
const gracePeriodEntry = this.gracePeriodEntries.get(userId);
|
|
3222
|
+
if (gracePeriodEntry) {
|
|
3223
|
+
this.gracePeriodEntries.delete(userId);
|
|
3224
|
+
const presence2 = gracePeriodEntry.previousPresence;
|
|
3225
|
+
const device2 = {
|
|
3226
|
+
deviceId,
|
|
3227
|
+
type: deviceInfo?.type,
|
|
3228
|
+
connectedAt: now,
|
|
3229
|
+
lastActivity: now,
|
|
3230
|
+
metadata: deviceInfo?.metadata
|
|
3231
|
+
};
|
|
3232
|
+
presence2.devices.set(deviceId, device2);
|
|
3233
|
+
presence2.status = "online";
|
|
3234
|
+
presence2.lastActivity = now;
|
|
3235
|
+
this.userPresences.set(userId, presence2);
|
|
3236
|
+
this.deviceToUser.set(deviceId, userId);
|
|
3237
|
+
return presence2;
|
|
3238
|
+
}
|
|
3239
|
+
let presence = this.userPresences.get(userId);
|
|
3240
|
+
if (!presence) {
|
|
3241
|
+
presence = {
|
|
3242
|
+
userId,
|
|
3243
|
+
status: "online",
|
|
3244
|
+
devices: new Map,
|
|
3245
|
+
firstConnectedAt: now,
|
|
3246
|
+
lastActivity: now,
|
|
3247
|
+
metadata: {}
|
|
3248
|
+
};
|
|
3249
|
+
this.userPresences.set(userId, presence);
|
|
3250
|
+
}
|
|
3251
|
+
const device = {
|
|
3252
|
+
deviceId,
|
|
3253
|
+
type: deviceInfo?.type,
|
|
3254
|
+
connectedAt: now,
|
|
3255
|
+
lastActivity: now,
|
|
3256
|
+
metadata: deviceInfo?.metadata
|
|
3257
|
+
};
|
|
3258
|
+
presence.devices.set(deviceId, device);
|
|
3259
|
+
presence.status = this.calculatePresenceStatus(presence.devices, now);
|
|
3260
|
+
presence.lastActivity = now;
|
|
3261
|
+
this.deviceToUser.set(deviceId, userId);
|
|
3262
|
+
return presence;
|
|
3263
|
+
}
|
|
3264
|
+
async disconnectDevice(userId, deviceId) {
|
|
3265
|
+
const presence = this.userPresences.get(userId);
|
|
3266
|
+
if (!presence) {
|
|
3267
|
+
return null;
|
|
3268
|
+
}
|
|
3269
|
+
presence.devices.delete(deviceId);
|
|
3270
|
+
this.deviceToUser.delete(deviceId);
|
|
3271
|
+
const now = Date.now();
|
|
3272
|
+
if (presence.devices.size === 0) {
|
|
3273
|
+
if (this.config.gracePeriod > 0) {
|
|
3274
|
+
this.gracePeriodEntries.set(userId, {
|
|
3275
|
+
userId,
|
|
3276
|
+
disconnectedAt: now,
|
|
3277
|
+
previousPresence: presence
|
|
3278
|
+
});
|
|
3279
|
+
this.userPresences.delete(userId);
|
|
3280
|
+
return null;
|
|
3281
|
+
} else {
|
|
3282
|
+
this.userPresences.delete(userId);
|
|
3283
|
+
return null;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
presence.status = this.calculatePresenceStatus(presence.devices, now);
|
|
3287
|
+
let mostRecentActivity = 0;
|
|
3288
|
+
for (const device of presence.devices.values()) {
|
|
3289
|
+
if (device.lastActivity > mostRecentActivity) {
|
|
3290
|
+
mostRecentActivity = device.lastActivity;
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
presence.lastActivity = mostRecentActivity;
|
|
3294
|
+
return presence;
|
|
3295
|
+
}
|
|
3296
|
+
async updateActivity(update) {
|
|
3297
|
+
const { userId, deviceId, timestamp } = update;
|
|
3298
|
+
const presence = this.userPresences.get(userId);
|
|
3299
|
+
if (!presence) {
|
|
3300
|
+
throw new Error(`User ${userId} not found in presence system`);
|
|
3301
|
+
}
|
|
3302
|
+
const device = presence.devices.get(deviceId);
|
|
3303
|
+
if (!device) {
|
|
3304
|
+
throw new Error(`Device ${deviceId} not found for user ${userId}`);
|
|
3305
|
+
}
|
|
3306
|
+
device.lastActivity = timestamp;
|
|
3307
|
+
if (timestamp > presence.lastActivity) {
|
|
3308
|
+
presence.lastActivity = timestamp;
|
|
3309
|
+
}
|
|
3310
|
+
const previousStatus = presence.status;
|
|
3311
|
+
presence.status = this.calculatePresenceStatus(presence.devices, timestamp);
|
|
3312
|
+
return presence;
|
|
3313
|
+
}
|
|
3314
|
+
async getUserPresence(userId) {
|
|
3315
|
+
return this.userPresences.get(userId) || null;
|
|
3316
|
+
}
|
|
3317
|
+
async getUsersPresence(userIds) {
|
|
3318
|
+
const result = new Map;
|
|
3319
|
+
for (const userId of userIds) {
|
|
3320
|
+
const presence = this.userPresences.get(userId);
|
|
3321
|
+
if (presence) {
|
|
3322
|
+
result.set(userId, presence);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
return result;
|
|
3326
|
+
}
|
|
3327
|
+
async getOnlineUsers() {
|
|
3328
|
+
const online = [];
|
|
3329
|
+
for (const presence of this.userPresences.values()) {
|
|
3330
|
+
if (presence.status === "online") {
|
|
3331
|
+
online.push(presence);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
return online;
|
|
3335
|
+
}
|
|
3336
|
+
async getUserDevices(userId) {
|
|
3337
|
+
const presence = this.userPresences.get(userId);
|
|
3338
|
+
if (!presence) {
|
|
3339
|
+
return [];
|
|
3340
|
+
}
|
|
3341
|
+
return Array.from(presence.devices.values());
|
|
3342
|
+
}
|
|
3343
|
+
async setUserStatus(userId, status) {
|
|
3344
|
+
const presence = this.userPresences.get(userId);
|
|
3345
|
+
if (!presence) {
|
|
3346
|
+
throw new Error(`User ${userId} not found in presence system`);
|
|
3347
|
+
}
|
|
3348
|
+
presence.status = status;
|
|
3349
|
+
return presence;
|
|
3350
|
+
}
|
|
3351
|
+
async processHeartbeats() {
|
|
3352
|
+
const now = Date.now();
|
|
3353
|
+
const changes = [];
|
|
3354
|
+
for (const [userId, presence] of this.userPresences.entries()) {
|
|
3355
|
+
const devicesToRemove = [];
|
|
3356
|
+
for (const [deviceId, device] of presence.devices.entries()) {
|
|
3357
|
+
if (this.shouldDisconnectDevice(device, now)) {
|
|
3358
|
+
devicesToRemove.push(deviceId);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
for (const deviceId of devicesToRemove) {
|
|
3362
|
+
presence.devices.delete(deviceId);
|
|
3363
|
+
this.deviceToUser.delete(deviceId);
|
|
3364
|
+
}
|
|
3365
|
+
const previousStatus = presence.status;
|
|
3366
|
+
const newStatus = this.calculatePresenceStatus(presence.devices, now);
|
|
3367
|
+
if (newStatus !== previousStatus) {
|
|
3368
|
+
presence.status = newStatus;
|
|
3369
|
+
changes.push({
|
|
3370
|
+
userId,
|
|
3371
|
+
previousStatus,
|
|
3372
|
+
status: newStatus,
|
|
3373
|
+
timestamp: now,
|
|
3374
|
+
reason: "heartbeat_timeout"
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
if (presence.devices.size === 0) {
|
|
3378
|
+
if (this.config.gracePeriod > 0) {
|
|
3379
|
+
this.gracePeriodEntries.set(userId, {
|
|
3380
|
+
userId,
|
|
3381
|
+
disconnectedAt: now,
|
|
3382
|
+
previousPresence: presence
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
this.userPresences.delete(userId);
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
const expiredGracePeriods = [];
|
|
3389
|
+
for (const [userId, entry] of this.gracePeriodEntries.entries()) {
|
|
3390
|
+
const gracePeriodElapsed = now - entry.disconnectedAt;
|
|
3391
|
+
if (gracePeriodElapsed > this.config.gracePeriod) {
|
|
3392
|
+
expiredGracePeriods.push(userId);
|
|
3393
|
+
changes.push({
|
|
3394
|
+
userId,
|
|
3395
|
+
previousStatus: entry.previousPresence.status,
|
|
3396
|
+
status: "offline",
|
|
3397
|
+
timestamp: now,
|
|
3398
|
+
reason: "grace_period_expired"
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
for (const userId of expiredGracePeriods) {
|
|
3403
|
+
this.gracePeriodEntries.delete(userId);
|
|
3404
|
+
}
|
|
3405
|
+
return changes;
|
|
3406
|
+
}
|
|
3407
|
+
async clear() {
|
|
3408
|
+
this.userPresences.clear();
|
|
3409
|
+
this.deviceToUser.clear();
|
|
3410
|
+
this.gracePeriodEntries.clear();
|
|
3411
|
+
}
|
|
3412
|
+
getGracePeriodCount() {
|
|
3413
|
+
return this.gracePeriodEntries.size;
|
|
3414
|
+
}
|
|
3415
|
+
getConnectedDeviceCount() {
|
|
3416
|
+
return this.deviceToUser.size;
|
|
3417
|
+
}
|
|
3418
|
+
getUniqueUserCount() {
|
|
3419
|
+
return this.userPresences.size + this.gracePeriodEntries.size;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3173
3422
|
// src/adapters/mock/MockTransportAdapter.ts
|
|
3174
3423
|
class MockTransportAdapter {
|
|
3175
3424
|
state = "disconnected";
|
|
@@ -3180,11 +3429,13 @@ class MockTransportAdapter {
|
|
|
3180
3429
|
simulateLatency = 0;
|
|
3181
3430
|
shouldFailConnection = false;
|
|
3182
3431
|
connectedUrl = null;
|
|
3432
|
+
connectionAttempts = 0;
|
|
3183
3433
|
constructor(options) {
|
|
3184
3434
|
this.simulateLatency = options?.simulateLatency ?? 0;
|
|
3185
3435
|
this.shouldFailConnection = options?.shouldFailConnection ?? false;
|
|
3186
3436
|
}
|
|
3187
3437
|
async connect(url, _options) {
|
|
3438
|
+
this.connectionAttempts++;
|
|
3188
3439
|
if (this.shouldFailConnection) {
|
|
3189
3440
|
this.state = "disconnected";
|
|
3190
3441
|
const error = new Error("Mock connection failed");
|
|
@@ -3234,8 +3485,8 @@ class MockTransportAdapter {
|
|
|
3234
3485
|
isConnected() {
|
|
3235
3486
|
return this.state === "connected";
|
|
3236
3487
|
}
|
|
3237
|
-
simulateMessage(message) {
|
|
3238
|
-
if (this.state !== "connected") {
|
|
3488
|
+
simulateMessage(message, bypassConnectionCheck = false) {
|
|
3489
|
+
if (!bypassConnectionCheck && this.state !== "connected") {
|
|
3239
3490
|
throw new Error("Cannot simulate message when not connected");
|
|
3240
3491
|
}
|
|
3241
3492
|
this.messageHandlers.forEach((handler) => handler(message));
|
|
@@ -3256,6 +3507,72 @@ class MockTransportAdapter {
|
|
|
3256
3507
|
getConnectedUrl() {
|
|
3257
3508
|
return this.connectedUrl;
|
|
3258
3509
|
}
|
|
3510
|
+
getConnectionAttempts() {
|
|
3511
|
+
return this.connectionAttempts;
|
|
3512
|
+
}
|
|
3513
|
+
resetConnectionAttempts() {
|
|
3514
|
+
this.connectionAttempts = 0;
|
|
3515
|
+
}
|
|
3516
|
+
async simulateConnection(clientId, options = {}) {
|
|
3517
|
+
if (this.state !== "connected") {
|
|
3518
|
+
this.state = "connected";
|
|
3519
|
+
}
|
|
3520
|
+
const message = {
|
|
3521
|
+
id: `conn-${clientId}`,
|
|
3522
|
+
type: "connection",
|
|
3523
|
+
payload: {
|
|
3524
|
+
clientId,
|
|
3525
|
+
authenticated: options.authenticated ?? false,
|
|
3526
|
+
userId: options.userId,
|
|
3527
|
+
metadata: options.metadata
|
|
3528
|
+
},
|
|
3529
|
+
timestamp: Date.now()
|
|
3530
|
+
};
|
|
3531
|
+
this.simulateMessage(message, true);
|
|
3532
|
+
}
|
|
3533
|
+
async simulateDisconnect(clientId, reason = "Normal closure") {
|
|
3534
|
+
const message = {
|
|
3535
|
+
id: `disconnect-${clientId}`,
|
|
3536
|
+
type: "disconnect",
|
|
3537
|
+
payload: {
|
|
3538
|
+
clientId,
|
|
3539
|
+
reason
|
|
3540
|
+
},
|
|
3541
|
+
timestamp: Date.now()
|
|
3542
|
+
};
|
|
3543
|
+
this.simulateMessage(message, true);
|
|
3544
|
+
}
|
|
3545
|
+
async simulateClientMessage(clientId, messageType, messagePayload = {}) {
|
|
3546
|
+
const message = {
|
|
3547
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
3548
|
+
type: "server_message",
|
|
3549
|
+
payload: {
|
|
3550
|
+
clientId,
|
|
3551
|
+
type: messageType,
|
|
3552
|
+
...messagePayload
|
|
3553
|
+
},
|
|
3554
|
+
timestamp: Date.now()
|
|
3555
|
+
};
|
|
3556
|
+
this.simulateMessage(message, true);
|
|
3557
|
+
}
|
|
3558
|
+
getSentMessages(clientId) {
|
|
3559
|
+
return this.messageQueue.filter((msg) => {
|
|
3560
|
+
const payload = msg.payload;
|
|
3561
|
+
return payload?.clientId === clientId;
|
|
3562
|
+
}).map((msg) => {
|
|
3563
|
+
const { clientId: _, ...rest } = msg.payload;
|
|
3564
|
+
return rest;
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
simulateIncomingMessage(message) {
|
|
3568
|
+
const fullMessage = {
|
|
3569
|
+
id: message.id || `msg-${Date.now()}`,
|
|
3570
|
+
type: message.type,
|
|
3571
|
+
payload: message.payload || {},
|
|
3572
|
+
timestamp: message.timestamp || Date.now()
|
|
3573
|
+
};
|
|
3574
|
+
this.messageHandlers.forEach((handler) => handler(fullMessage));
|
|
3575
|
+
}
|
|
3259
3576
|
}
|
|
3260
3577
|
// src/adapters/mock/MockStorageAdapter.ts
|
|
3261
3578
|
class MockStorageAdapter {
|
|
@@ -3635,10 +3952,10 @@ class WebSocketTransportAdapter {
|
|
|
3635
3952
|
}
|
|
3636
3953
|
setAuthAdapter(auth) {
|
|
3637
3954
|
this.authAdapter = auth;
|
|
3638
|
-
if (auth.isAuthRequired) {
|
|
3955
|
+
if (auth.isAuthRequired && !this.config.requireAuth) {
|
|
3639
3956
|
this.config.requireAuth = auth.isAuthRequired();
|
|
3640
3957
|
}
|
|
3641
|
-
if (auth.getAuthTimeout) {
|
|
3958
|
+
if (auth.getAuthTimeout && this.config.authTimeout === 5000) {
|
|
3642
3959
|
this.config.authTimeout = auth.getAuthTimeout();
|
|
3643
3960
|
}
|
|
3644
3961
|
}
|
|
@@ -4400,12 +4717,14 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4400
4717
|
storage;
|
|
4401
4718
|
roomManager;
|
|
4402
4719
|
lockManager;
|
|
4720
|
+
presenceManager;
|
|
4403
4721
|
config;
|
|
4404
4722
|
clients = new Map;
|
|
4405
4723
|
clientMessageHandlers = new Map;
|
|
4406
4724
|
running = false;
|
|
4407
4725
|
initialized = false;
|
|
4408
4726
|
mode;
|
|
4727
|
+
heartbeatIntervalId;
|
|
4409
4728
|
experimental;
|
|
4410
4729
|
constructor(config) {
|
|
4411
4730
|
super();
|
|
@@ -4415,6 +4734,7 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4415
4734
|
this.storage = config.storage;
|
|
4416
4735
|
this.roomManager = config.roomManager;
|
|
4417
4736
|
this.lockManager = config.lockManager;
|
|
4737
|
+
this.presenceManager = config.presenceManager;
|
|
4418
4738
|
this.experimental = new ExperimentalAPI({
|
|
4419
4739
|
config: config.experimental || {
|
|
4420
4740
|
enableBroadcast: false,
|
|
@@ -4438,6 +4758,50 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4438
4758
|
this.transport.onMessage(this.handleTransportMessage.bind(this));
|
|
4439
4759
|
this.transport.onError(this.handleTransportError.bind(this));
|
|
4440
4760
|
this.transport.onClose(this.handleTransportClose.bind(this));
|
|
4761
|
+
if (this.presenceManager?.isEnabled()) {
|
|
4762
|
+
this.startHeartbeatProcessor();
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
startHeartbeatProcessor() {
|
|
4766
|
+
if (!this.presenceManager) {
|
|
4767
|
+
return;
|
|
4768
|
+
}
|
|
4769
|
+
const config = this.presenceManager.getConfig();
|
|
4770
|
+
const interval = config.heartbeatInterval;
|
|
4771
|
+
this.heartbeatIntervalId = setInterval(async () => {
|
|
4772
|
+
try {
|
|
4773
|
+
const changes = await this.presenceManager.processHeartbeats();
|
|
4774
|
+
for (const change of changes) {
|
|
4775
|
+
await this.emit("presence_changed", {
|
|
4776
|
+
userId: change.userId,
|
|
4777
|
+
status: change.status,
|
|
4778
|
+
timestamp: change.timestamp
|
|
4779
|
+
});
|
|
4780
|
+
if (config.broadcastPresenceUpdates) {
|
|
4781
|
+
await this.experimental.broadcastAuthenticated({
|
|
4782
|
+
type: "presence:status_changed",
|
|
4783
|
+
payload: {
|
|
4784
|
+
userId: change.userId,
|
|
4785
|
+
status: change.status,
|
|
4786
|
+
previousStatus: change.previousStatus,
|
|
4787
|
+
reason: change.reason
|
|
4788
|
+
}
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
} catch (error) {
|
|
4793
|
+
await this.emit("error", {
|
|
4794
|
+
error,
|
|
4795
|
+
context: "heartbeat_processor"
|
|
4796
|
+
});
|
|
4797
|
+
}
|
|
4798
|
+
}, interval);
|
|
4799
|
+
}
|
|
4800
|
+
stopHeartbeatProcessor() {
|
|
4801
|
+
if (this.heartbeatIntervalId) {
|
|
4802
|
+
clearInterval(this.heartbeatIntervalId);
|
|
4803
|
+
this.heartbeatIntervalId = undefined;
|
|
4804
|
+
}
|
|
4441
4805
|
}
|
|
4442
4806
|
async start(port) {
|
|
4443
4807
|
if (this.mode === "integration") {
|
|
@@ -4496,6 +4860,7 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4496
4860
|
return;
|
|
4497
4861
|
}
|
|
4498
4862
|
this.running = false;
|
|
4863
|
+
this.stopHeartbeatProcessor();
|
|
4499
4864
|
for (const [clientId] of Array.from(this.clients)) {
|
|
4500
4865
|
await this.disconnectClient(clientId, "Server shutting down");
|
|
4501
4866
|
}
|
|
@@ -4506,6 +4871,9 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4506
4871
|
}
|
|
4507
4872
|
this.clients.clear();
|
|
4508
4873
|
this.clientMessageHandlers.clear();
|
|
4874
|
+
if (this.presenceManager) {
|
|
4875
|
+
await this.presenceManager.clear();
|
|
4876
|
+
}
|
|
4509
4877
|
await this.emit("stopped", {});
|
|
4510
4878
|
}
|
|
4511
4879
|
async handleTransportMessage(message) {
|
|
@@ -4517,11 +4885,16 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4517
4885
|
await this.handleClientAuthenticatedMessage(message);
|
|
4518
4886
|
return;
|
|
4519
4887
|
}
|
|
4888
|
+
if (message.type === "disconnect") {
|
|
4889
|
+
const payload2 = message.payload;
|
|
4890
|
+
await this.disconnectClient(payload2.clientId, payload2.reason || "Client disconnected");
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4520
4893
|
const payload = message.payload;
|
|
4521
|
-
const { clientId, ...clientMessage } = payload;
|
|
4894
|
+
const { clientId, type, ...clientMessage } = payload;
|
|
4522
4895
|
const clientMsg = {
|
|
4523
4896
|
id: message.id,
|
|
4524
|
-
type: message.type,
|
|
4897
|
+
type: type || message.type,
|
|
4525
4898
|
payload: clientMessage,
|
|
4526
4899
|
timestamp: message.timestamp
|
|
4527
4900
|
};
|
|
@@ -4544,16 +4917,50 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4544
4917
|
}
|
|
4545
4918
|
async handleConnectionMessage(message) {
|
|
4546
4919
|
const payload = message.payload;
|
|
4920
|
+
const roomIds = new Set;
|
|
4547
4921
|
const client = {
|
|
4548
4922
|
id: payload.clientId,
|
|
4549
4923
|
userId: payload.userId || "",
|
|
4550
|
-
|
|
4924
|
+
roomIds,
|
|
4925
|
+
get roomId() {
|
|
4926
|
+
return roomIds.size > 0 ? roomIds.values().next().value || null : null;
|
|
4927
|
+
},
|
|
4551
4928
|
authenticated: payload.authenticated,
|
|
4552
4929
|
connectedAt: Date.now()
|
|
4553
4930
|
};
|
|
4554
4931
|
this.clients.set(payload.clientId, client);
|
|
4555
4932
|
this.clientMessageHandlers.set(payload.clientId, this.createClientMessageHandler(payload.clientId));
|
|
4556
4933
|
await this.emit("client_connected", { client });
|
|
4934
|
+
if (payload.authenticated && this.presenceManager?.isEnabled() && payload.userId) {
|
|
4935
|
+
try {
|
|
4936
|
+
await this.presenceManager.connectDevice(payload.userId, payload.clientId, {
|
|
4937
|
+
connectedAt: Date.now(),
|
|
4938
|
+
lastActivity: Date.now(),
|
|
4939
|
+
metadata: payload.metadata
|
|
4940
|
+
});
|
|
4941
|
+
await this.emit("presence_device_connected", {
|
|
4942
|
+
userId: payload.userId,
|
|
4943
|
+
deviceId: payload.clientId,
|
|
4944
|
+
timestamp: Date.now()
|
|
4945
|
+
});
|
|
4946
|
+
const presenceConfig = this.presenceManager.getConfig();
|
|
4947
|
+
if (presenceConfig.broadcastPresenceUpdates) {
|
|
4948
|
+
await this.experimental.broadcastAuthenticated({
|
|
4949
|
+
type: "presence:user_online",
|
|
4950
|
+
payload: {
|
|
4951
|
+
userId: payload.userId,
|
|
4952
|
+
deviceId: payload.clientId,
|
|
4953
|
+
status: "online"
|
|
4954
|
+
}
|
|
4955
|
+
});
|
|
4956
|
+
}
|
|
4957
|
+
} catch (error) {
|
|
4958
|
+
await this.emit("error", {
|
|
4959
|
+
error,
|
|
4960
|
+
context: "presence_connect_device"
|
|
4961
|
+
});
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4557
4964
|
if (payload.authenticated) {
|
|
4558
4965
|
await this.emit("client_authenticated", {
|
|
4559
4966
|
clientId: payload.clientId,
|
|
@@ -4582,10 +4989,14 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4582
4989
|
}
|
|
4583
4990
|
}
|
|
4584
4991
|
async addClient(clientId) {
|
|
4992
|
+
const roomIds = new Set;
|
|
4585
4993
|
const client = {
|
|
4586
4994
|
id: clientId,
|
|
4587
4995
|
userId: "",
|
|
4588
|
-
|
|
4996
|
+
roomIds,
|
|
4997
|
+
get roomId() {
|
|
4998
|
+
return roomIds.size > 0 ? roomIds.values().next().value || null : null;
|
|
4999
|
+
},
|
|
4589
5000
|
authenticated: false,
|
|
4590
5001
|
connectedAt: Date.now()
|
|
4591
5002
|
};
|
|
@@ -4598,9 +5009,36 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4598
5009
|
if (!client) {
|
|
4599
5010
|
return;
|
|
4600
5011
|
}
|
|
4601
|
-
if (client.
|
|
4602
|
-
|
|
4603
|
-
|
|
5012
|
+
if (this.presenceManager?.isEnabled() && client.userId) {
|
|
5013
|
+
try {
|
|
5014
|
+
const presence = await this.presenceManager.disconnectDevice(client.userId, clientId);
|
|
5015
|
+
await this.emit("presence_device_disconnected", {
|
|
5016
|
+
userId: client.userId,
|
|
5017
|
+
deviceId: clientId,
|
|
5018
|
+
timestamp: Date.now()
|
|
5019
|
+
});
|
|
5020
|
+
const presenceConfig = this.presenceManager.getConfig();
|
|
5021
|
+
if (presenceConfig.broadcastPresenceUpdates && (!presence || presence.status === "offline")) {
|
|
5022
|
+
await this.experimental.broadcastAuthenticated({
|
|
5023
|
+
type: "presence:user_offline",
|
|
5024
|
+
payload: {
|
|
5025
|
+
userId: client.userId,
|
|
5026
|
+
deviceId: clientId,
|
|
5027
|
+
status: presence?.status || "offline",
|
|
5028
|
+
gracePeriod: presenceConfig.gracePeriod
|
|
5029
|
+
}
|
|
5030
|
+
});
|
|
5031
|
+
}
|
|
5032
|
+
} catch (error) {
|
|
5033
|
+
await this.emit("error", {
|
|
5034
|
+
error,
|
|
5035
|
+
context: "presence_disconnect_device"
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
for (const roomId of client.roomIds) {
|
|
5040
|
+
await this.roomManager.leaveRoom(roomId, client.userId);
|
|
5041
|
+
await this.emit("client_left_room", { clientId, roomId });
|
|
4604
5042
|
}
|
|
4605
5043
|
await this.lockManager.releaseUserLocks(client.userId);
|
|
4606
5044
|
this.clients.delete(clientId);
|
|
@@ -4622,7 +5060,7 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4622
5060
|
await this.handleJoinRoom(clientId, message.payload);
|
|
4623
5061
|
break;
|
|
4624
5062
|
case "leave_room":
|
|
4625
|
-
await this.handleLeaveRoom(clientId);
|
|
5063
|
+
await this.handleLeaveRoom(clientId, message.payload);
|
|
4626
5064
|
break;
|
|
4627
5065
|
case "broadcast_event":
|
|
4628
5066
|
await this.handleBroadcastEvent(clientId, message.payload);
|
|
@@ -4635,6 +5073,9 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4635
5073
|
break;
|
|
4636
5074
|
case "ping":
|
|
4637
5075
|
await this.sendToClient(clientId, { type: "pong", timestamp: Date.now() });
|
|
5076
|
+
if (this.presenceManager?.isEnabled() && client.userId) {
|
|
5077
|
+
await this.updatePresenceActivity(client.userId, clientId, "heartbeat");
|
|
5078
|
+
}
|
|
4638
5079
|
break;
|
|
4639
5080
|
default:
|
|
4640
5081
|
await this.sendToClient(clientId, {
|
|
@@ -4682,9 +5123,9 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4682
5123
|
return;
|
|
4683
5124
|
}
|
|
4684
5125
|
try {
|
|
4685
|
-
if (client.roomId) {
|
|
4686
|
-
await this.
|
|
4687
|
-
|
|
5126
|
+
if (client.roomIds.has(payload.roomId)) {
|
|
5127
|
+
await this.sendToClient(clientId, { type: "error", error: "Already in this room" });
|
|
5128
|
+
return;
|
|
4688
5129
|
}
|
|
4689
5130
|
let roomState = await this.roomManager.getRoomState(payload.roomId);
|
|
4690
5131
|
if (!roomState) {
|
|
@@ -4705,7 +5146,7 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4705
5146
|
permissions: roomState.room.permissions || ["read", "write"]
|
|
4706
5147
|
};
|
|
4707
5148
|
await this.roomManager.joinRoom(payload.roomId, user);
|
|
4708
|
-
client.
|
|
5149
|
+
client.roomIds.add(payload.roomId);
|
|
4709
5150
|
await this.emit("client_joined_room", { clientId, roomId: payload.roomId });
|
|
4710
5151
|
await this.sendToClient(clientId, {
|
|
4711
5152
|
type: "room_joined",
|
|
@@ -4723,20 +5164,28 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4723
5164
|
await this.sendToClient(clientId, { type: "error", error: error.message });
|
|
4724
5165
|
}
|
|
4725
5166
|
}
|
|
4726
|
-
async handleLeaveRoom(clientId) {
|
|
5167
|
+
async handleLeaveRoom(clientId, payload) {
|
|
4727
5168
|
const client = this.clients.get(clientId);
|
|
4728
|
-
if (!client
|
|
5169
|
+
if (!client) {
|
|
4729
5170
|
return;
|
|
4730
5171
|
}
|
|
4731
|
-
const
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
5172
|
+
const roomsToLeave = payload?.roomId ? [payload.roomId] : Array.from(client.roomIds);
|
|
5173
|
+
if (roomsToLeave.length === 0) {
|
|
5174
|
+
return;
|
|
5175
|
+
}
|
|
5176
|
+
for (const roomId of roomsToLeave) {
|
|
5177
|
+
if (!client.roomIds.has(roomId)) {
|
|
5178
|
+
continue;
|
|
5179
|
+
}
|
|
5180
|
+
await this.roomManager.leaveRoom(roomId, client.userId);
|
|
5181
|
+
client.roomIds.delete(roomId);
|
|
5182
|
+
await this.emit("client_left_room", { clientId, roomId });
|
|
5183
|
+
await this.sendToClient(clientId, { type: "room_left", roomId });
|
|
5184
|
+
}
|
|
4736
5185
|
}
|
|
4737
5186
|
async handleBroadcastEvent(clientId, payload) {
|
|
4738
5187
|
const client = this.clients.get(clientId);
|
|
4739
|
-
if (!client || client.
|
|
5188
|
+
if (!client || !client.roomIds.has(payload.roomId)) {
|
|
4740
5189
|
await this.sendToClient(clientId, { type: "error", error: "Not in specified room" });
|
|
4741
5190
|
return;
|
|
4742
5191
|
}
|
|
@@ -4759,7 +5208,7 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4759
5208
|
}
|
|
4760
5209
|
async handleLockRequest(clientId, payload) {
|
|
4761
5210
|
const client = this.clients.get(clientId);
|
|
4762
|
-
if (!client || client.
|
|
5211
|
+
if (!client || !client.roomIds.has(payload.roomId)) {
|
|
4763
5212
|
await this.sendToClient(clientId, { type: "error", error: "Not in specified room" });
|
|
4764
5213
|
return;
|
|
4765
5214
|
}
|
|
@@ -4789,15 +5238,15 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4789
5238
|
await this.lockManager.releaseLock(payload.lockId);
|
|
4790
5239
|
await this.emit("lock_released", { lockId: payload.lockId, clientId });
|
|
4791
5240
|
await this.sendToClient(clientId, { type: "lock_released", lockId: payload.lockId });
|
|
4792
|
-
|
|
4793
|
-
await this.roomManager.broadcastToRoom(
|
|
5241
|
+
for (const roomId of client.roomIds) {
|
|
5242
|
+
await this.roomManager.broadcastToRoom(roomId, {
|
|
4794
5243
|
id: this.generateId(),
|
|
4795
5244
|
type: "lock_status",
|
|
4796
5245
|
timestamp: Date.now(),
|
|
4797
5246
|
userId: client.userId,
|
|
4798
|
-
roomId
|
|
5247
|
+
roomId,
|
|
4799
5248
|
data: { lockId: payload.lockId, action: "released" },
|
|
4800
|
-
metadata: { userId: client.userId, timestamp: Date.now(), roomId
|
|
5249
|
+
metadata: { userId: client.userId, timestamp: Date.now(), roomId }
|
|
4801
5250
|
});
|
|
4802
5251
|
}
|
|
4803
5252
|
} catch (error) {
|
|
@@ -4840,6 +5289,32 @@ class BaseServer extends TypedEventEmitter {
|
|
|
4840
5289
|
isInitialized() {
|
|
4841
5290
|
return this.initialized;
|
|
4842
5291
|
}
|
|
5292
|
+
async updatePresenceActivity(userId, deviceId, activityType) {
|
|
5293
|
+
if (!this.presenceManager?.isEnabled()) {
|
|
5294
|
+
return;
|
|
5295
|
+
}
|
|
5296
|
+
try {
|
|
5297
|
+
await this.presenceManager.updateActivity({
|
|
5298
|
+
userId,
|
|
5299
|
+
deviceId,
|
|
5300
|
+
timestamp: Date.now(),
|
|
5301
|
+
activityType
|
|
5302
|
+
});
|
|
5303
|
+
await this.emit("presence_activity", {
|
|
5304
|
+
userId,
|
|
5305
|
+
deviceId,
|
|
5306
|
+
timestamp: Date.now()
|
|
5307
|
+
});
|
|
5308
|
+
} catch (error) {
|
|
5309
|
+
await this.emit("error", {
|
|
5310
|
+
error,
|
|
5311
|
+
context: "presence_update_activity"
|
|
5312
|
+
});
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
getPresenceManager() {
|
|
5316
|
+
return this.presenceManager;
|
|
5317
|
+
}
|
|
4843
5318
|
}
|
|
4844
5319
|
// src/server/ServerBuilder.ts
|
|
4845
5320
|
class ServerBuilder {
|
|
@@ -4848,6 +5323,7 @@ class ServerBuilder {
|
|
|
4848
5323
|
storage;
|
|
4849
5324
|
roomManager;
|
|
4850
5325
|
lockManager;
|
|
5326
|
+
presenceManager;
|
|
4851
5327
|
defaultRoomConfig;
|
|
4852
5328
|
httpServer;
|
|
4853
5329
|
webSocketPath;
|
|
@@ -4873,6 +5349,10 @@ class ServerBuilder {
|
|
|
4873
5349
|
this.lockManager = lockManager;
|
|
4874
5350
|
return this;
|
|
4875
5351
|
}
|
|
5352
|
+
withPresenceManager(presenceManager) {
|
|
5353
|
+
this.presenceManager = presenceManager;
|
|
5354
|
+
return this;
|
|
5355
|
+
}
|
|
4876
5356
|
withDefaultRoomConfig(config) {
|
|
4877
5357
|
this.defaultRoomConfig = config;
|
|
4878
5358
|
return this;
|
|
@@ -4909,6 +5389,7 @@ class ServerBuilder {
|
|
|
4909
5389
|
storage: this.storage,
|
|
4910
5390
|
roomManager: this.roomManager,
|
|
4911
5391
|
lockManager: this.lockManager,
|
|
5392
|
+
presenceManager: this.presenceManager,
|
|
4912
5393
|
defaultRoomConfig: this.defaultRoomConfig || {
|
|
4913
5394
|
maxUsers: 50,
|
|
4914
5395
|
maxHistory: 100,
|
|
@@ -4944,4 +5425,4 @@ export {
|
|
|
4944
5425
|
BaseClient
|
|
4945
5426
|
};
|
|
4946
5427
|
|
|
4947
|
-
//# debugId=
|
|
5428
|
+
//# debugId=5B674B605D2E752364756E2164756E21
|