wexa-chat 0.1.21 → 0.1.22

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 CHANGED
@@ -286,9 +286,97 @@ You can also send:
286
286
  ```ts
287
287
  send({ type: 'leave-conversation', conversationId });
288
288
  send({ type: 'typing', conversationId, isTyping: true });
289
+
290
+ // New: User-based subscriptions
291
+ send({ type: 'subscribe-to-user', userId: 'user-123' });
292
+ send({ type: 'unsubscribe-from-user', userId: 'user-123' });
289
293
  ```
290
294
 
291
- ## Events (server → client)
295
+ ## User-Based Subscriptions
296
+
297
+ The chat core now supports subscribing to all messages for a specific user, allowing you to receive messages from all conversations involving that user without needing to join each conversation individually.
298
+
299
+ ### Server-Side Usage
300
+
301
+ The transport layer exposes new methods for user-based subscriptions:
302
+
303
+ ```ts
304
+ // Access the transport from your chat instance
305
+ const { transport } = await getChatInstance();
306
+
307
+ // Subscribe a socket to all messages for a user
308
+ transport.subscribeToUser(userId, socketId);
309
+
310
+ // Unsubscribe a socket from a user's messages
311
+ transport.unsubscribeFromUser(userId, socketId);
312
+
313
+ // Check if a socket is subscribed to a user
314
+ const isSubscribed = transport.isSubscribedToUser(userId, socketId);
315
+
316
+ // Fan out a message to all subscribers of a user
317
+ transport.fanoutToUser(userId, payload, (toSocketId, event) => {
318
+ // Your emit logic here
319
+ });
320
+ ```
321
+
322
+ ### Client-Side Usage
323
+
324
+ Subscribe to all messages for a user:
325
+
326
+ ```tsx
327
+ import { useEffect } from 'react';
328
+ import { useSocket } from 'wexa-chat/client';
329
+
330
+ export function GlobalMessageListener({ userId }: { userId: string }) {
331
+ const { isConnected, send, onEvent } = useSocket();
332
+
333
+ // Subscribe to user messages
334
+ useEffect(() => {
335
+ if (isConnected && userId) {
336
+ send({ type: 'subscribe-to-user', userId });
337
+ }
338
+
339
+ return () => {
340
+ if (isConnected && userId) {
341
+ send({ type: 'unsubscribe-from-user', userId });
342
+ }
343
+ };
344
+ }, [isConnected, userId, send]);
345
+
346
+ // Listen for messages
347
+ useEffect(() => {
348
+ const off = onEvent((event) => {
349
+ if (event.type === 'message:created') {
350
+ // Handle message from any conversation involving the subscribed user
351
+ console.log('New message:', event.message);
352
+ }
353
+ });
354
+
355
+ return off;
356
+ }, [onEvent]);
357
+
358
+ return null; // This component just listens for messages
359
+ }
360
+ ```
361
+
362
+ ### Use Cases
363
+
364
+ - **Global notifications**: Receive notifications for all messages involving a user
365
+ - **Unread message tracking**: Track unread messages across all conversations
366
+ - **Real-time updates**: Keep conversation lists updated without joining each conversation
367
+ - **Background message sync**: Sync messages even when not actively viewing conversations
368
+
369
+ ## Events
370
+
371
+ ### Client → Server
372
+
373
+ - `join-conversation` — `{ type: 'join-conversation', conversationId }`
374
+ - `leave-conversation` — `{ type: 'leave-conversation', conversationId }`
375
+ - `typing` — `{ type: 'typing', conversationId, isTyping: boolean }`
376
+ - `subscribe-to-user` — `{ type: 'subscribe-to-user', userId }` *(New)*
377
+ - `unsubscribe-from-user` — `{ type: 'unsubscribe-from-user', userId }` *(New)*
378
+
379
+ ### Server → Client
292
380
 
293
381
  - `message:created` — `{ type: 'message:created', message }`
294
382
  - `conversation:read` — `{ type: 'conversation:read', conversationId, messageId, participantModel, participantId, at }`
package/dist/index.d.cts CHANGED
@@ -315,7 +315,7 @@ declare function createMessagesService(models: {
315
315
  }, hooks?: MessageServiceHooks): MessagesService;
316
316
 
317
317
  /**
318
- * In-memory subscription maps for conversations and sockets
318
+ * In-memory subscription maps for conversations, users, and sockets
319
319
  */
320
320
  /**
321
321
  * Subscribe a socket to a conversation
@@ -341,6 +341,36 @@ declare function cleanupLocal(socketId: string): void;
341
341
  * @param emitter Function to emit events to socket IDs
342
342
  */
343
343
  declare function fanoutLocal(conversationId: string, payload: any, emitter: (toSocketId: string, event: any) => void): void;
344
+ /**
345
+ * Subscribe a socket to all messages for a user
346
+ * @param userId The user ID to subscribe to
347
+ * @param socketId The socket ID to subscribe
348
+ */
349
+ declare function subscribeToUser(userId: string, socketId: string): void;
350
+ /**
351
+ * Unsubscribe a socket from a user's messages
352
+ * @param userId The user ID to unsubscribe from
353
+ * @param socketId The socket ID to unsubscribe
354
+ */
355
+ declare function unsubscribeFromUser(userId: string, socketId: string): void;
356
+ /**
357
+ * Check if a socket is subscribed to a user
358
+ * @param userId The user ID to check
359
+ * @param socketId The socket ID to check
360
+ */
361
+ declare function isSubscribedToUser(userId: string, socketId: string): boolean;
362
+ /**
363
+ * Get all sockets subscribed to a user
364
+ * @param userId The user ID
365
+ */
366
+ declare function getSocketsForUser(userId: string): Set<string>;
367
+ /**
368
+ * Fan out a message to all local subscribers of a user
369
+ * @param userId The user ID to fan out to
370
+ * @param payload The payload to send
371
+ * @param emitter Function to emit events to socket IDs
372
+ */
373
+ declare function fanoutToUser(userId: string, payload: any, emitter: (toSocketId: string, event: any) => void): void;
344
374
 
345
375
  interface SocketConfig {
346
376
  path?: string;
@@ -355,6 +385,7 @@ declare class SocketTransport {
355
385
  private sockets;
356
386
  private users;
357
387
  private joinedConversations;
388
+ private subscribedToUsers;
358
389
  private authenticate?;
359
390
  private ioShim;
360
391
  constructor(server: Server, transport: Transport, config?: SocketConfig);
@@ -391,6 +422,11 @@ interface Transport {
391
422
  unsubscribeLocal: typeof unsubscribeLocal;
392
423
  cleanupLocal: typeof cleanupLocal;
393
424
  fanoutLocal: typeof fanoutLocal;
425
+ subscribeToUser: typeof subscribeToUser;
426
+ unsubscribeFromUser: typeof unsubscribeFromUser;
427
+ isSubscribedToUser: typeof isSubscribedToUser;
428
+ getSocketsForUser: typeof getSocketsForUser;
429
+ fanoutToUser: typeof fanoutToUser;
394
430
  publishToConversation: (conversationId: string, payload: any) => Promise<number>;
395
431
  startRedisListener: (onEvent: (conversationId: string, payload: any) => void) => (() => void) | null;
396
432
  }
package/dist/index.d.ts CHANGED
@@ -315,7 +315,7 @@ declare function createMessagesService(models: {
315
315
  }, hooks?: MessageServiceHooks): MessagesService;
316
316
 
317
317
  /**
318
- * In-memory subscription maps for conversations and sockets
318
+ * In-memory subscription maps for conversations, users, and sockets
319
319
  */
320
320
  /**
321
321
  * Subscribe a socket to a conversation
@@ -341,6 +341,36 @@ declare function cleanupLocal(socketId: string): void;
341
341
  * @param emitter Function to emit events to socket IDs
342
342
  */
343
343
  declare function fanoutLocal(conversationId: string, payload: any, emitter: (toSocketId: string, event: any) => void): void;
344
+ /**
345
+ * Subscribe a socket to all messages for a user
346
+ * @param userId The user ID to subscribe to
347
+ * @param socketId The socket ID to subscribe
348
+ */
349
+ declare function subscribeToUser(userId: string, socketId: string): void;
350
+ /**
351
+ * Unsubscribe a socket from a user's messages
352
+ * @param userId The user ID to unsubscribe from
353
+ * @param socketId The socket ID to unsubscribe
354
+ */
355
+ declare function unsubscribeFromUser(userId: string, socketId: string): void;
356
+ /**
357
+ * Check if a socket is subscribed to a user
358
+ * @param userId The user ID to check
359
+ * @param socketId The socket ID to check
360
+ */
361
+ declare function isSubscribedToUser(userId: string, socketId: string): boolean;
362
+ /**
363
+ * Get all sockets subscribed to a user
364
+ * @param userId The user ID
365
+ */
366
+ declare function getSocketsForUser(userId: string): Set<string>;
367
+ /**
368
+ * Fan out a message to all local subscribers of a user
369
+ * @param userId The user ID to fan out to
370
+ * @param payload The payload to send
371
+ * @param emitter Function to emit events to socket IDs
372
+ */
373
+ declare function fanoutToUser(userId: string, payload: any, emitter: (toSocketId: string, event: any) => void): void;
344
374
 
345
375
  interface SocketConfig {
346
376
  path?: string;
@@ -355,6 +385,7 @@ declare class SocketTransport {
355
385
  private sockets;
356
386
  private users;
357
387
  private joinedConversations;
388
+ private subscribedToUsers;
358
389
  private authenticate?;
359
390
  private ioShim;
360
391
  constructor(server: Server, transport: Transport, config?: SocketConfig);
@@ -391,6 +422,11 @@ interface Transport {
391
422
  unsubscribeLocal: typeof unsubscribeLocal;
392
423
  cleanupLocal: typeof cleanupLocal;
393
424
  fanoutLocal: typeof fanoutLocal;
425
+ subscribeToUser: typeof subscribeToUser;
426
+ unsubscribeFromUser: typeof unsubscribeFromUser;
427
+ isSubscribedToUser: typeof isSubscribedToUser;
428
+ getSocketsForUser: typeof getSocketsForUser;
429
+ fanoutToUser: typeof fanoutToUser;
394
430
  publishToConversation: (conversationId: string, payload: any) => Promise<number>;
395
431
  startRedisListener: (onEvent: (conversationId: string, payload: any) => void) => (() => void) | null;
396
432
  }
package/dist/index.js CHANGED
@@ -665,6 +665,8 @@ function createMessagesService(models, hooks = {}) {
665
665
  // src/transport/localSubs.ts
666
666
  var convSubs = /* @__PURE__ */ new Map();
667
667
  var socketSubs = /* @__PURE__ */ new Map();
668
+ var userSubs = /* @__PURE__ */ new Map();
669
+ var socketUserSubs = /* @__PURE__ */ new Map();
668
670
  function subscribeLocal(conversationId, socketId) {
669
671
  var _a, _b;
670
672
  if (!convSubs.has(conversationId)) {
@@ -706,6 +708,19 @@ function cleanupLocal(socketId) {
706
708
  }
707
709
  socketSubs.delete(socketId);
708
710
  }
711
+ const users = socketUserSubs.get(socketId);
712
+ if (users) {
713
+ for (const userId of users) {
714
+ const sockets = userSubs.get(userId);
715
+ if (sockets) {
716
+ sockets.delete(socketId);
717
+ if (sockets.size === 0) {
718
+ userSubs.delete(userId);
719
+ }
720
+ }
721
+ }
722
+ socketUserSubs.delete(socketId);
723
+ }
709
724
  }
710
725
  function fanoutLocal(conversationId, payload, emitter) {
711
726
  const sockets = convSubs.get(conversationId);
@@ -719,6 +734,52 @@ function fanoutLocal(conversationId, payload, emitter) {
719
734
  }
720
735
  }
721
736
  }
737
+ function subscribeToUser(userId, socketId) {
738
+ var _a, _b;
739
+ if (!userSubs.has(userId)) {
740
+ userSubs.set(userId, /* @__PURE__ */ new Set());
741
+ }
742
+ (_a = userSubs.get(userId)) == null ? void 0 : _a.add(socketId);
743
+ if (!socketUserSubs.has(socketId)) {
744
+ socketUserSubs.set(socketId, /* @__PURE__ */ new Set());
745
+ }
746
+ (_b = socketUserSubs.get(socketId)) == null ? void 0 : _b.add(userId);
747
+ }
748
+ function unsubscribeFromUser(userId, socketId) {
749
+ const sockets = userSubs.get(userId);
750
+ if (sockets) {
751
+ sockets.delete(socketId);
752
+ if (sockets.size === 0) {
753
+ userSubs.delete(userId);
754
+ }
755
+ }
756
+ const users = socketUserSubs.get(socketId);
757
+ if (users) {
758
+ users.delete(userId);
759
+ if (users.size === 0) {
760
+ socketUserSubs.delete(socketId);
761
+ }
762
+ }
763
+ }
764
+ function isSubscribedToUser(userId, socketId) {
765
+ var _a;
766
+ return ((_a = socketUserSubs.get(socketId)) == null ? void 0 : _a.has(userId)) || false;
767
+ }
768
+ function getSocketsForUser(userId) {
769
+ return userSubs.get(userId) || /* @__PURE__ */ new Set();
770
+ }
771
+ function fanoutToUser(userId, payload, emitter) {
772
+ const sockets = userSubs.get(userId);
773
+ if (sockets) {
774
+ for (const socketId of sockets) {
775
+ try {
776
+ emitter(socketId, payload);
777
+ } catch (error) {
778
+ console.error(`Error emitting to socket ${socketId}:`, error);
779
+ }
780
+ }
781
+ }
782
+ }
722
783
  var SHARED_CLIENT = null;
723
784
  var SHARED_CFG_FINGERPRINT = null;
724
785
  function createRedisClient(config) {
@@ -815,6 +876,8 @@ var SocketTransport = class {
815
876
  this.sockets = /* @__PURE__ */ new Map();
816
877
  this.users = /* @__PURE__ */ new Map();
817
878
  this.joinedConversations = /* @__PURE__ */ new Map();
879
+ // socketId -> convIds
880
+ this.subscribedToUsers = /* @__PURE__ */ new Map();
818
881
  // Socket.IO-like shim for getIO().sockets.sockets.get(id).emit('chat-event', payload)
819
882
  this.ioShim = {
820
883
  sockets: {
@@ -924,6 +987,7 @@ var SocketTransport = class {
924
987
  this.joinedConversations.set(socketId, /* @__PURE__ */ new Set());
925
988
  syncShim();
926
989
  ws$1.on("message", (raw) => {
990
+ var _a;
927
991
  try {
928
992
  const msg = JSON.parse(raw.toString());
929
993
  const type = msg == null ? void 0 : msg.type;
@@ -994,6 +1058,17 @@ var SocketTransport = class {
994
1058
  }
995
1059
  });
996
1060
  this.transport.publishToConversation(convId, payload);
1061
+ } else if (type === "subscribe-to-user" && typeof msg.userId === "string") {
1062
+ const userId2 = msg.userId;
1063
+ this.transport.subscribeToUser(userId2, socketId);
1064
+ if (!this.subscribedToUsers.has(socketId)) {
1065
+ this.subscribedToUsers.set(socketId, /* @__PURE__ */ new Set());
1066
+ }
1067
+ this.subscribedToUsers.get(socketId).add(userId2);
1068
+ } else if (type === "unsubscribe-from-user" && typeof msg.userId === "string") {
1069
+ const userId2 = msg.userId;
1070
+ this.transport.unsubscribeFromUser(userId2, socketId);
1071
+ (_a = this.subscribedToUsers.get(socketId)) == null ? void 0 : _a.delete(userId2);
997
1072
  }
998
1073
  } catch {
999
1074
  }
@@ -1022,6 +1097,13 @@ var SocketTransport = class {
1022
1097
  });
1023
1098
  }
1024
1099
  }
1100
+ const subscribedUsers = this.subscribedToUsers.get(socketId);
1101
+ if (subscribedUsers) {
1102
+ for (const userId2 of subscribedUsers) {
1103
+ this.transport.unsubscribeFromUser(userId2, socketId);
1104
+ }
1105
+ this.subscribedToUsers.delete(socketId);
1106
+ }
1025
1107
  this.transport.cleanupLocal(socketId);
1026
1108
  this.sockets.delete(socketId);
1027
1109
  this.users.delete(socketId);
@@ -1086,6 +1168,12 @@ function createTransport(config, server) {
1086
1168
  unsubscribeLocal,
1087
1169
  cleanupLocal,
1088
1170
  fanoutLocal,
1171
+ // User subscription methods
1172
+ subscribeToUser,
1173
+ unsubscribeFromUser,
1174
+ isSubscribedToUser,
1175
+ getSocketsForUser,
1176
+ fanoutToUser,
1089
1177
  // Redis methods
1090
1178
  publishToConversation: (conversationId, payload) => publishToConversation(redisClient, conversationId, payload),
1091
1179
  startRedisListener: (onEvent) => startRedisListener(redisClient, onEvent)