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

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();
@@ -4235,7 +4349,15 @@ class WebSocketServerTransportAdapter {
4235
4349
  if (client.ws.readyState !== import_websocket.default.OPEN) {
4236
4350
  throw new Error(`Client ${clientId} connection is not open`);
4237
4351
  }
4238
- const messageToSend = {
4352
+ const messageToSend = message.type === "server_message" && clientMessage.type ? {
4353
+ id: message.id,
4354
+ type: clientMessage.type,
4355
+ payload: clientMessage.payload || (() => {
4356
+ const { type, ...rest } = clientMessage;
4357
+ return rest;
4358
+ })(),
4359
+ timestamp: message.timestamp
4360
+ } : {
4239
4361
  ...message,
4240
4362
  payload: clientMessage
4241
4363
  };
@@ -4778,6 +4900,107 @@ class ClientBuilder {
4778
4900
  return new BaseClient(config);
4779
4901
  }
4780
4902
  }
4903
+ // src/client/PresenceClient.ts
4904
+ class PresenceClient extends TypedEventEmitter {
4905
+ client;
4906
+ wsUrl;
4907
+ autoReconnectEnabled;
4908
+ globalRoomId;
4909
+ reconnectTimeout;
4910
+ constructor(config) {
4911
+ super();
4912
+ this.wsUrl = config.wsUrl;
4913
+ this.autoReconnectEnabled = config.autoReconnect ?? true;
4914
+ this.globalRoomId = config.globalRoomId ?? "__global_presence__";
4915
+ this.client = new BaseClient(config.clientConfig);
4916
+ this.setupEventForwarding();
4917
+ if (config.autoConnect) {
4918
+ this.connect();
4919
+ }
4920
+ }
4921
+ async connect() {
4922
+ try {
4923
+ await this.client.connect(this.wsUrl);
4924
+ await this.client.joinRoom(this.globalRoomId);
4925
+ await this.emit("connected", {});
4926
+ } catch (error) {
4927
+ await this.emit("error", { error });
4928
+ throw error;
4929
+ }
4930
+ }
4931
+ async disconnect() {
4932
+ this.autoReconnectEnabled = false;
4933
+ if (this.reconnectTimeout) {
4934
+ clearTimeout(this.reconnectTimeout);
4935
+ this.reconnectTimeout = undefined;
4936
+ }
4937
+ try {
4938
+ await this.client.leaveRoom();
4939
+ await this.client.disconnect();
4940
+ await this.emit("disconnected", {});
4941
+ } catch (error) {
4942
+ await this.emit("error", { error });
4943
+ }
4944
+ }
4945
+ async getOnlineUsers() {
4946
+ throw new Error("getOnlineUsers() requires a REST API endpoint. " + "Implement this method based on your server API.");
4947
+ }
4948
+ async setStatus(status, message) {
4949
+ throw new Error("setStatus() requires a REST API endpoint. " + "Implement this method based on your server API.");
4950
+ }
4951
+ async setVisibility(visible) {
4952
+ throw new Error("setVisibility() requires a REST API endpoint. " + "Implement this method based on your server API.");
4953
+ }
4954
+ getClient() {
4955
+ return this.client;
4956
+ }
4957
+ isConnected() {
4958
+ return this.client.connectionState === "connected";
4959
+ }
4960
+ setupEventForwarding() {
4961
+ this.client.on("presence:user_online", (data) => {
4962
+ if (data && typeof data === "object") {
4963
+ const payload = data;
4964
+ this.emit("userOnline", {
4965
+ userId: payload.userId,
4966
+ devices: payload.devices || []
4967
+ });
4968
+ }
4969
+ });
4970
+ this.client.on("presence:user_offline", (data) => {
4971
+ if (data && typeof data === "object") {
4972
+ const payload = data;
4973
+ this.emit("userOffline", { userId: payload.userId });
4974
+ }
4975
+ });
4976
+ this.client.on("presence:status_changed", (data) => {
4977
+ if (data && typeof data === "object") {
4978
+ const payload = data;
4979
+ this.emit("statusChanged", {
4980
+ userId: payload.userId,
4981
+ status: payload.status,
4982
+ statusMessage: payload.statusMessage
4983
+ });
4984
+ }
4985
+ });
4986
+ this.client.on("disconnected", () => {
4987
+ this.emit("disconnected", {});
4988
+ if (this.autoReconnectEnabled) {
4989
+ this.reconnectTimeout = setTimeout(() => {
4990
+ this.connect().catch((error) => {
4991
+ this.emit("error", { error });
4992
+ });
4993
+ }, 5000);
4994
+ }
4995
+ });
4996
+ this.client.on("error", (data) => {
4997
+ if (data && typeof data === "object" && "error" in data) {
4998
+ const payload = data;
4999
+ this.emit("error", { error: payload.error });
5000
+ }
5001
+ });
5002
+ }
5003
+ }
4781
5004
  // src/server/ExperimentalAPI.ts
4782
5005
  class ExperimentalAPI {
4783
5006
  config;
@@ -4916,6 +5139,8 @@ class BaseServer extends TypedEventEmitter {
4916
5139
  config;
4917
5140
  clients = new Map;
4918
5141
  clientMessageHandlers = new Map;
5142
+ clientUserMap = new Map;
5143
+ userClientMap = new Map;
4919
5144
  running = false;
4920
5145
  initialized = false;
4921
5146
  mode;
@@ -5066,6 +5291,8 @@ class BaseServer extends TypedEventEmitter {
5066
5291
  }
5067
5292
  this.clients.clear();
5068
5293
  this.clientMessageHandlers.clear();
5294
+ this.clientUserMap.clear();
5295
+ this.userClientMap.clear();
5069
5296
  if (this.presenceManager) {
5070
5297
  await this.presenceManager.clear();
5071
5298
  }
@@ -5169,6 +5396,11 @@ class BaseServer extends TypedEventEmitter {
5169
5396
  if (client) {
5170
5397
  client.authenticated = true;
5171
5398
  client.userId = payload.userId;
5399
+ this.clientUserMap.set(payload.clientId, payload.userId);
5400
+ if (!this.userClientMap.has(payload.userId)) {
5401
+ this.userClientMap.set(payload.userId, new Set);
5402
+ }
5403
+ this.userClientMap.get(payload.userId).add(payload.clientId);
5172
5404
  await this.emit("client_authenticated", {
5173
5405
  clientId: payload.clientId,
5174
5406
  userId: payload.userId
@@ -5204,6 +5436,16 @@ class BaseServer extends TypedEventEmitter {
5204
5436
  if (!client) {
5205
5437
  return;
5206
5438
  }
5439
+ if (client.userId) {
5440
+ this.clientUserMap.delete(clientId);
5441
+ const clientSet = this.userClientMap.get(client.userId);
5442
+ if (clientSet) {
5443
+ clientSet.delete(clientId);
5444
+ if (clientSet.size === 0) {
5445
+ this.userClientMap.delete(client.userId);
5446
+ }
5447
+ }
5448
+ }
5207
5449
  if (this.presenceManager?.isEnabled() && client.userId) {
5208
5450
  try {
5209
5451
  const presence = await this.presenceManager.disconnectDevice(client.userId, clientId);
@@ -5247,6 +5489,9 @@ class BaseServer extends TypedEventEmitter {
5247
5489
  if (!client) {
5248
5490
  return;
5249
5491
  }
5492
+ if (this.presenceManager?.isEnabled() && client.userId && client.authenticated) {
5493
+ await this.updatePresenceActivity(client.userId, clientId, "message");
5494
+ }
5250
5495
  switch (message.type) {
5251
5496
  case "authenticate":
5252
5497
  await this.handleAuthenticate(clientId, message.payload);
@@ -5268,9 +5513,6 @@ class BaseServer extends TypedEventEmitter {
5268
5513
  break;
5269
5514
  case "ping":
5270
5515
  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
5516
  break;
5275
5517
  default:
5276
5518
  await this.sendToClient(clientId, {
@@ -5510,6 +5752,15 @@ class BaseServer extends TypedEventEmitter {
5510
5752
  getPresenceManager() {
5511
5753
  return this.presenceManager;
5512
5754
  }
5755
+ getUserIdFromClientId(clientId) {
5756
+ return this.clientUserMap.get(clientId);
5757
+ }
5758
+ getClientIdsForUser(userId) {
5759
+ return Array.from(this.userClientMap.get(userId) || []);
5760
+ }
5761
+ getDeviceIdFromClientId(clientId) {
5762
+ return clientId;
5763
+ }
5513
5764
  }
5514
5765
  // src/server/ServerBuilder.ts
5515
5766
  class ServerBuilder {
@@ -5599,7 +5850,44 @@ class ServerBuilder {
5599
5850
  trackUsageMetrics: false
5600
5851
  }
5601
5852
  };
5602
- return new BaseServer(config);
5853
+ const server = new BaseServer(config);
5854
+ if (this.presenceManager instanceof DefaultPresenceManager) {
5855
+ this.presenceManager.setServer(server);
5856
+ }
5857
+ if (this.presenceManager instanceof DefaultPresenceManager) {
5858
+ this.wirePresenceExtensionHooks(server, this.presenceManager);
5859
+ }
5860
+ return server;
5861
+ }
5862
+ wirePresenceExtensionHooks(server, presenceManager) {
5863
+ server.on("started", async () => {
5864
+ await presenceManager.initializeExtensions();
5865
+ });
5866
+ server.on("client_joined_room", async ({ clientId, roomId }) => {
5867
+ const userId = server.getUserIdFromClientId(clientId);
5868
+ if (userId && presenceManager.isEnabled()) {
5869
+ const extensions = presenceManager.extensions;
5870
+ if (extensions) {
5871
+ for (const ext of extensions) {
5872
+ await ext.onRoomJoined?.(userId, roomId, clientId);
5873
+ }
5874
+ }
5875
+ }
5876
+ });
5877
+ server.on("client_left_room", async ({ clientId, roomId }) => {
5878
+ const userId = server.getUserIdFromClientId(clientId);
5879
+ if (userId && presenceManager.isEnabled()) {
5880
+ const extensions = presenceManager.extensions;
5881
+ if (extensions) {
5882
+ for (const ext of extensions) {
5883
+ await ext.onRoomLeft?.(userId, roomId, clientId);
5884
+ }
5885
+ }
5886
+ }
5887
+ });
5888
+ server.on("stopped", async () => {
5889
+ await presenceManager.destroyExtensions();
5890
+ });
5603
5891
  }
5604
5892
  }
5605
5893
  export {
@@ -5609,6 +5897,7 @@ export {
5609
5897
  ServerBuilder,
5610
5898
  RoomManager,
5611
5899
  PresenceManager,
5900
+ PresenceClient,
5612
5901
  MockTransportAdapter,
5613
5902
  MockStorageAdapter,
5614
5903
  MockAuthAdapter,
@@ -5623,4 +5912,4 @@ export {
5623
5912
  BaseClient
5624
5913
  };
5625
5914
 
5626
- //# debugId=202CBDD84215CE1F64756E2164756E21
5915
+ //# debugId=2756317EEC93367264756E2164756E21