@principal-ai/control-tower-core 0.1.12 → 0.1.13

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/dist/index.mjs CHANGED
@@ -3213,11 +3213,28 @@ class DefaultPresenceManager extends PresenceManager {
3213
3213
  userPresences = new Map;
3214
3214
  deviceToUser = new Map;
3215
3215
  gracePeriodEntries = new Map;
3216
+ extensions = [];
3217
+ server;
3216
3218
  constructor(config) {
3217
3219
  super(config);
3220
+ this.extensions = config?.extensions || [];
3221
+ }
3222
+ setServer(server) {
3223
+ this.server = server;
3224
+ }
3225
+ async initializeExtensions() {
3226
+ for (const ext of this.extensions) {
3227
+ await ext.initialize?.();
3228
+ }
3229
+ }
3230
+ async destroyExtensions() {
3231
+ for (const ext of this.extensions) {
3232
+ await ext.destroy?.();
3233
+ }
3218
3234
  }
3219
3235
  async connectDevice(userId, deviceId, deviceInfo) {
3220
3236
  const now = Date.now();
3237
+ const isFirstDevice = !this.userPresences.has(userId) && !this.gracePeriodEntries.has(userId);
3221
3238
  const gracePeriodEntry = this.gracePeriodEntries.get(userId);
3222
3239
  if (gracePeriodEntry) {
3223
3240
  this.gracePeriodEntries.delete(userId);
@@ -3234,9 +3251,31 @@ class DefaultPresenceManager extends PresenceManager {
3234
3251
  presence2.lastActivity = now;
3235
3252
  this.userPresences.set(userId, presence2);
3236
3253
  this.deviceToUser.set(deviceId, userId);
3254
+ if (isFirstDevice) {
3255
+ for (const ext of this.extensions) {
3256
+ await ext.onUserOnline?.(userId, deviceId, deviceInfo?.metadata || {});
3257
+ }
3258
+ } else {
3259
+ for (const ext of this.extensions) {
3260
+ await ext.onDeviceConnected?.(userId, deviceId, deviceInfo?.metadata || {});
3261
+ }
3262
+ }
3263
+ if (this.config.broadcastPresenceUpdates && this.server) {
3264
+ const experimental = this.server.experimental;
3265
+ await experimental?.broadcastAuthenticated({
3266
+ type: "presence:user_online",
3267
+ payload: {
3268
+ userId,
3269
+ deviceId,
3270
+ status: "online",
3271
+ timestamp: now
3272
+ }
3273
+ });
3274
+ }
3237
3275
  return presence2;
3238
3276
  }
3239
3277
  let presence = this.userPresences.get(userId);
3278
+ const wasOnline = presence !== undefined;
3240
3279
  if (!presence) {
3241
3280
  presence = {
3242
3281
  userId,
@@ -3259,6 +3298,27 @@ class DefaultPresenceManager extends PresenceManager {
3259
3298
  presence.status = this.calculatePresenceStatus(presence.devices, now);
3260
3299
  presence.lastActivity = now;
3261
3300
  this.deviceToUser.set(deviceId, userId);
3301
+ if (!wasOnline) {
3302
+ for (const ext of this.extensions) {
3303
+ await ext.onUserOnline?.(userId, deviceId, deviceInfo?.metadata || {});
3304
+ }
3305
+ } else {
3306
+ for (const ext of this.extensions) {
3307
+ await ext.onDeviceConnected?.(userId, deviceId, deviceInfo?.metadata || {});
3308
+ }
3309
+ }
3310
+ if (!wasOnline && this.config.broadcastPresenceUpdates && this.server) {
3311
+ const experimental = this.server.experimental;
3312
+ await experimental?.broadcastAuthenticated({
3313
+ type: "presence:user_online",
3314
+ payload: {
3315
+ userId,
3316
+ deviceId,
3317
+ status: "online",
3318
+ timestamp: now
3319
+ }
3320
+ });
3321
+ }
3262
3322
  return presence;
3263
3323
  }
3264
3324
  async disconnectDevice(userId, deviceId) {
@@ -3269,7 +3329,24 @@ class DefaultPresenceManager extends PresenceManager {
3269
3329
  presence.devices.delete(deviceId);
3270
3330
  this.deviceToUser.delete(deviceId);
3271
3331
  const now = Date.now();
3272
- if (presence.devices.size === 0) {
3332
+ const isLastDevice = presence.devices.size === 0;
3333
+ if (isLastDevice) {
3334
+ for (const ext of this.extensions) {
3335
+ await ext.onUserOffline?.(userId, deviceId);
3336
+ }
3337
+ if (this.config.broadcastPresenceUpdates && this.server) {
3338
+ const experimental = this.server.experimental;
3339
+ await experimental?.broadcastAuthenticated({
3340
+ type: "presence:user_offline",
3341
+ payload: {
3342
+ userId,
3343
+ deviceId,
3344
+ status: "offline",
3345
+ timestamp: now,
3346
+ gracePeriod: this.config.gracePeriod
3347
+ }
3348
+ });
3349
+ }
3273
3350
  if (this.config.gracePeriod > 0) {
3274
3351
  this.gracePeriodEntries.set(userId, {
3275
3352
  userId,
@@ -3283,6 +3360,9 @@ class DefaultPresenceManager extends PresenceManager {
3283
3360
  return null;
3284
3361
  }
3285
3362
  }
3363
+ for (const ext of this.extensions) {
3364
+ await ext.onDeviceDisconnected?.(userId, deviceId);
3365
+ }
3286
3366
  presence.status = this.calculatePresenceStatus(presence.devices, now);
3287
3367
  let mostRecentActivity = 0;
3288
3368
  for (const device of presence.devices.values()) {
@@ -3294,7 +3374,7 @@ class DefaultPresenceManager extends PresenceManager {
3294
3374
  return presence;
3295
3375
  }
3296
3376
  async updateActivity(update) {
3297
- const { userId, deviceId, timestamp } = update;
3377
+ const { userId, deviceId, timestamp, activityType } = update;
3298
3378
  const presence = this.userPresences.get(userId);
3299
3379
  if (!presence) {
3300
3380
  throw new Error(`User ${userId} not found in presence system`);
@@ -3309,10 +3389,30 @@ class DefaultPresenceManager extends PresenceManager {
3309
3389
  }
3310
3390
  const previousStatus = presence.status;
3311
3391
  presence.status = this.calculatePresenceStatus(presence.devices, timestamp);
3392
+ for (const ext of this.extensions) {
3393
+ await ext.onActivity?.(userId, deviceId, activityType || "heartbeat");
3394
+ }
3312
3395
  return presence;
3313
3396
  }
3314
3397
  async getUserPresence(userId) {
3315
- return this.userPresences.get(userId) || null;
3398
+ const basePresence = this.userPresences.get(userId);
3399
+ if (!basePresence) {
3400
+ return null;
3401
+ }
3402
+ if (this.extensions.length > 0) {
3403
+ const extendedData = {};
3404
+ for (const ext of this.extensions) {
3405
+ const data = await ext.getExtendedData?.(userId);
3406
+ if (data) {
3407
+ Object.assign(extendedData, data);
3408
+ }
3409
+ }
3410
+ return {
3411
+ ...basePresence,
3412
+ extended: extendedData
3413
+ };
3414
+ }
3415
+ return basePresence;
3316
3416
  }
3317
3417
  async getUsersPresence(userIds) {
3318
3418
  const result = new Map;
@@ -3328,7 +3428,20 @@ class DefaultPresenceManager extends PresenceManager {
3328
3428
  const online = [];
3329
3429
  for (const presence of this.userPresences.values()) {
3330
3430
  if (presence.status === "online") {
3331
- online.push(presence);
3431
+ let visible = true;
3432
+ for (const ext of this.extensions) {
3433
+ const shouldShow = await ext.shouldBeVisible?.(presence.userId);
3434
+ if (shouldShow === false) {
3435
+ visible = false;
3436
+ break;
3437
+ }
3438
+ }
3439
+ if (visible) {
3440
+ const fullPresence = await this.getUserPresence(presence.userId);
3441
+ if (fullPresence) {
3442
+ online.push(fullPresence);
3443
+ }
3444
+ }
3332
3445
  }
3333
3446
  }
3334
3447
  return online;
@@ -3405,6 +3518,7 @@ class DefaultPresenceManager extends PresenceManager {
3405
3518
  return changes;
3406
3519
  }
3407
3520
  async clear() {
3521
+ await this.destroyExtensions();
3408
3522
  this.userPresences.clear();
3409
3523
  this.deviceToUser.clear();
3410
3524
  this.gracePeriodEntries.clear();
@@ -4778,6 +4892,107 @@ class ClientBuilder {
4778
4892
  return new BaseClient(config);
4779
4893
  }
4780
4894
  }
4895
+ // src/client/PresenceClient.ts
4896
+ class PresenceClient extends TypedEventEmitter {
4897
+ client;
4898
+ wsUrl;
4899
+ autoReconnectEnabled;
4900
+ globalRoomId;
4901
+ reconnectTimeout;
4902
+ constructor(config) {
4903
+ super();
4904
+ this.wsUrl = config.wsUrl;
4905
+ this.autoReconnectEnabled = config.autoReconnect ?? true;
4906
+ this.globalRoomId = config.globalRoomId ?? "__global_presence__";
4907
+ this.client = new BaseClient(config.clientConfig);
4908
+ this.setupEventForwarding();
4909
+ if (config.autoConnect) {
4910
+ this.connect();
4911
+ }
4912
+ }
4913
+ async connect() {
4914
+ try {
4915
+ await this.client.connect(this.wsUrl);
4916
+ await this.client.joinRoom(this.globalRoomId);
4917
+ await this.emit("connected", {});
4918
+ } catch (error) {
4919
+ await this.emit("error", { error });
4920
+ throw error;
4921
+ }
4922
+ }
4923
+ async disconnect() {
4924
+ this.autoReconnectEnabled = false;
4925
+ if (this.reconnectTimeout) {
4926
+ clearTimeout(this.reconnectTimeout);
4927
+ this.reconnectTimeout = undefined;
4928
+ }
4929
+ try {
4930
+ await this.client.leaveRoom();
4931
+ await this.client.disconnect();
4932
+ await this.emit("disconnected", {});
4933
+ } catch (error) {
4934
+ await this.emit("error", { error });
4935
+ }
4936
+ }
4937
+ async getOnlineUsers() {
4938
+ throw new Error("getOnlineUsers() requires a REST API endpoint. " + "Implement this method based on your server API.");
4939
+ }
4940
+ async setStatus(status, message) {
4941
+ throw new Error("setStatus() requires a REST API endpoint. " + "Implement this method based on your server API.");
4942
+ }
4943
+ async setVisibility(visible) {
4944
+ throw new Error("setVisibility() requires a REST API endpoint. " + "Implement this method based on your server API.");
4945
+ }
4946
+ getClient() {
4947
+ return this.client;
4948
+ }
4949
+ isConnected() {
4950
+ return this.client.connectionState === "connected";
4951
+ }
4952
+ setupEventForwarding() {
4953
+ this.client.on("presence:user_online", (data) => {
4954
+ if (data && typeof data === "object") {
4955
+ const payload = data;
4956
+ this.emit("userOnline", {
4957
+ userId: payload.userId,
4958
+ devices: payload.devices || []
4959
+ });
4960
+ }
4961
+ });
4962
+ this.client.on("presence:user_offline", (data) => {
4963
+ if (data && typeof data === "object") {
4964
+ const payload = data;
4965
+ this.emit("userOffline", { userId: payload.userId });
4966
+ }
4967
+ });
4968
+ this.client.on("presence:status_changed", (data) => {
4969
+ if (data && typeof data === "object") {
4970
+ const payload = data;
4971
+ this.emit("statusChanged", {
4972
+ userId: payload.userId,
4973
+ status: payload.status,
4974
+ statusMessage: payload.statusMessage
4975
+ });
4976
+ }
4977
+ });
4978
+ this.client.on("disconnected", () => {
4979
+ this.emit("disconnected", {});
4980
+ if (this.autoReconnectEnabled) {
4981
+ this.reconnectTimeout = setTimeout(() => {
4982
+ this.connect().catch((error) => {
4983
+ this.emit("error", { error });
4984
+ });
4985
+ }, 5000);
4986
+ }
4987
+ });
4988
+ this.client.on("error", (data) => {
4989
+ if (data && typeof data === "object" && "error" in data) {
4990
+ const payload = data;
4991
+ this.emit("error", { error: payload.error });
4992
+ }
4993
+ });
4994
+ }
4995
+ }
4781
4996
  // src/server/ExperimentalAPI.ts
4782
4997
  class ExperimentalAPI {
4783
4998
  config;
@@ -4916,6 +5131,8 @@ class BaseServer extends TypedEventEmitter {
4916
5131
  config;
4917
5132
  clients = new Map;
4918
5133
  clientMessageHandlers = new Map;
5134
+ clientUserMap = new Map;
5135
+ userClientMap = new Map;
4919
5136
  running = false;
4920
5137
  initialized = false;
4921
5138
  mode;
@@ -5066,6 +5283,8 @@ class BaseServer extends TypedEventEmitter {
5066
5283
  }
5067
5284
  this.clients.clear();
5068
5285
  this.clientMessageHandlers.clear();
5286
+ this.clientUserMap.clear();
5287
+ this.userClientMap.clear();
5069
5288
  if (this.presenceManager) {
5070
5289
  await this.presenceManager.clear();
5071
5290
  }
@@ -5169,6 +5388,11 @@ class BaseServer extends TypedEventEmitter {
5169
5388
  if (client) {
5170
5389
  client.authenticated = true;
5171
5390
  client.userId = payload.userId;
5391
+ this.clientUserMap.set(payload.clientId, payload.userId);
5392
+ if (!this.userClientMap.has(payload.userId)) {
5393
+ this.userClientMap.set(payload.userId, new Set);
5394
+ }
5395
+ this.userClientMap.get(payload.userId).add(payload.clientId);
5172
5396
  await this.emit("client_authenticated", {
5173
5397
  clientId: payload.clientId,
5174
5398
  userId: payload.userId
@@ -5204,6 +5428,16 @@ class BaseServer extends TypedEventEmitter {
5204
5428
  if (!client) {
5205
5429
  return;
5206
5430
  }
5431
+ if (client.userId) {
5432
+ this.clientUserMap.delete(clientId);
5433
+ const clientSet = this.userClientMap.get(client.userId);
5434
+ if (clientSet) {
5435
+ clientSet.delete(clientId);
5436
+ if (clientSet.size === 0) {
5437
+ this.userClientMap.delete(client.userId);
5438
+ }
5439
+ }
5440
+ }
5207
5441
  if (this.presenceManager?.isEnabled() && client.userId) {
5208
5442
  try {
5209
5443
  const presence = await this.presenceManager.disconnectDevice(client.userId, clientId);
@@ -5247,6 +5481,9 @@ class BaseServer extends TypedEventEmitter {
5247
5481
  if (!client) {
5248
5482
  return;
5249
5483
  }
5484
+ if (this.presenceManager?.isEnabled() && client.userId && client.authenticated) {
5485
+ await this.updatePresenceActivity(client.userId, clientId, "message");
5486
+ }
5250
5487
  switch (message.type) {
5251
5488
  case "authenticate":
5252
5489
  await this.handleAuthenticate(clientId, message.payload);
@@ -5268,9 +5505,6 @@ class BaseServer extends TypedEventEmitter {
5268
5505
  break;
5269
5506
  case "ping":
5270
5507
  await this.sendToClient(clientId, { type: "pong", timestamp: Date.now() });
5271
- if (this.presenceManager?.isEnabled() && client.userId) {
5272
- await this.updatePresenceActivity(client.userId, clientId, "heartbeat");
5273
- }
5274
5508
  break;
5275
5509
  default:
5276
5510
  await this.sendToClient(clientId, {
@@ -5510,6 +5744,15 @@ class BaseServer extends TypedEventEmitter {
5510
5744
  getPresenceManager() {
5511
5745
  return this.presenceManager;
5512
5746
  }
5747
+ getUserIdFromClientId(clientId) {
5748
+ return this.clientUserMap.get(clientId);
5749
+ }
5750
+ getClientIdsForUser(userId) {
5751
+ return Array.from(this.userClientMap.get(userId) || []);
5752
+ }
5753
+ getDeviceIdFromClientId(clientId) {
5754
+ return clientId;
5755
+ }
5513
5756
  }
5514
5757
  // src/server/ServerBuilder.ts
5515
5758
  class ServerBuilder {
@@ -5599,7 +5842,44 @@ class ServerBuilder {
5599
5842
  trackUsageMetrics: false
5600
5843
  }
5601
5844
  };
5602
- return new BaseServer(config);
5845
+ const server = new BaseServer(config);
5846
+ if (this.presenceManager instanceof DefaultPresenceManager) {
5847
+ this.presenceManager.setServer(server);
5848
+ }
5849
+ if (this.presenceManager instanceof DefaultPresenceManager) {
5850
+ this.wirePresenceExtensionHooks(server, this.presenceManager);
5851
+ }
5852
+ return server;
5853
+ }
5854
+ wirePresenceExtensionHooks(server, presenceManager) {
5855
+ server.on("started", async () => {
5856
+ await presenceManager.initializeExtensions();
5857
+ });
5858
+ server.on("client_joined_room", async ({ clientId, roomId }) => {
5859
+ const userId = server.getUserIdFromClientId(clientId);
5860
+ if (userId && presenceManager.isEnabled()) {
5861
+ const extensions = presenceManager.extensions;
5862
+ if (extensions) {
5863
+ for (const ext of extensions) {
5864
+ await ext.onRoomJoined?.(userId, roomId, clientId);
5865
+ }
5866
+ }
5867
+ }
5868
+ });
5869
+ server.on("client_left_room", async ({ clientId, roomId }) => {
5870
+ const userId = server.getUserIdFromClientId(clientId);
5871
+ if (userId && presenceManager.isEnabled()) {
5872
+ const extensions = presenceManager.extensions;
5873
+ if (extensions) {
5874
+ for (const ext of extensions) {
5875
+ await ext.onRoomLeft?.(userId, roomId, clientId);
5876
+ }
5877
+ }
5878
+ }
5879
+ });
5880
+ server.on("stopped", async () => {
5881
+ await presenceManager.destroyExtensions();
5882
+ });
5603
5883
  }
5604
5884
  }
5605
5885
  export {
@@ -5609,6 +5889,7 @@ export {
5609
5889
  ServerBuilder,
5610
5890
  RoomManager,
5611
5891
  PresenceManager,
5892
+ PresenceClient,
5612
5893
  MockTransportAdapter,
5613
5894
  MockStorageAdapter,
5614
5895
  MockAuthAdapter,
@@ -5623,4 +5904,4 @@ export {
5623
5904
  BaseClient
5624
5905
  };
5625
5906
 
5626
- //# debugId=202CBDD84215CE1F64756E2164756E21
5907
+ //# debugId=205D80A7D7E855D364756E2164756E21