chatly-sdk 0.0.5 → 0.0.6

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 (56) hide show
  1. package/CONTRIBUTING.md +658 -0
  2. package/IMPROVEMENTS.md +402 -0
  3. package/README.md +1538 -164
  4. package/dist/index.d.ts +430 -9
  5. package/dist/index.js +1420 -63
  6. package/examples/01-basic-chat/README.md +61 -0
  7. package/examples/01-basic-chat/index.js +58 -0
  8. package/examples/01-basic-chat/package.json +13 -0
  9. package/examples/02-group-chat/README.md +78 -0
  10. package/examples/02-group-chat/index.js +76 -0
  11. package/examples/02-group-chat/package.json +13 -0
  12. package/examples/03-offline-messaging/README.md +73 -0
  13. package/examples/03-offline-messaging/index.js +80 -0
  14. package/examples/03-offline-messaging/package.json +13 -0
  15. package/examples/04-live-chat/README.md +80 -0
  16. package/examples/04-live-chat/index.js +114 -0
  17. package/examples/04-live-chat/package.json +13 -0
  18. package/examples/05-hybrid-messaging/README.md +71 -0
  19. package/examples/05-hybrid-messaging/index.js +106 -0
  20. package/examples/05-hybrid-messaging/package.json +13 -0
  21. package/examples/06-postgresql-integration/README.md +101 -0
  22. package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
  23. package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
  24. package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
  25. package/examples/06-postgresql-integration/index.js +92 -0
  26. package/examples/06-postgresql-integration/package.json +14 -0
  27. package/examples/06-postgresql-integration/schema.sql +58 -0
  28. package/examples/08-customer-support/README.md +70 -0
  29. package/examples/08-customer-support/index.js +104 -0
  30. package/examples/08-customer-support/package.json +13 -0
  31. package/examples/README.md +105 -0
  32. package/jest.config.cjs +28 -0
  33. package/package.json +12 -8
  34. package/src/chat/ChatSession.ts +81 -0
  35. package/src/chat/GroupSession.ts +79 -0
  36. package/src/constants.ts +61 -0
  37. package/src/crypto/e2e.ts +0 -20
  38. package/src/index.ts +525 -63
  39. package/src/models/mediaTypes.ts +58 -0
  40. package/src/models/message.ts +4 -1
  41. package/src/transport/adapters.ts +51 -1
  42. package/src/transport/memoryTransport.ts +75 -13
  43. package/src/transport/websocketClient.ts +269 -21
  44. package/src/transport/websocketServer.ts +26 -26
  45. package/src/utils/errors.ts +97 -0
  46. package/src/utils/logger.ts +96 -0
  47. package/src/utils/mediaUtils.ts +235 -0
  48. package/src/utils/messageQueue.ts +162 -0
  49. package/src/utils/validation.ts +99 -0
  50. package/test/crypto.test.ts +122 -35
  51. package/test/sdk.test.ts +276 -0
  52. package/test/validation.test.ts +64 -0
  53. package/tsconfig.json +11 -10
  54. package/tsconfig.test.json +11 -0
  55. package/src/ChatManager.ts +0 -103
  56. package/src/crypto/keyManager.ts +0 -28
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { EventEmitter } from 'events';
1
2
  import type { User, StoredUser } from "./models/user.js";
2
3
  import type { Message } from "./models/message.js";
3
4
  import type { Group } from "./models/group.js";
5
+ import type { MediaAttachment } from "./models/mediaTypes.js";
4
6
  import type {
5
7
  UserStoreAdapter,
6
8
  MessageStoreAdapter,
@@ -11,29 +13,121 @@ import { ChatSession } from "./chat/ChatSession.js";
11
13
  import { GroupSession } from "./chat/GroupSession.js";
12
14
  import { generateIdentityKeyPair } from "./crypto/keys.js";
13
15
  import { generateUUID } from "./crypto/uuid.js";
16
+ import { logger, LogLevel } from "./utils/logger.js";
17
+ import { validateUsername, validateMessage, validateGroupName, validateGroupMembers } from "./utils/validation.js";
18
+ import { SessionError, StorageError, TransportError } from "./utils/errors.js";
19
+ import { MessageQueue } from "./utils/messageQueue.js";
20
+ import { EVENTS, ConnectionState } from "./constants.js";
14
21
 
15
22
  export interface ChatSDKConfig {
16
23
  userStore: UserStoreAdapter;
17
24
  messageStore: MessageStoreAdapter;
18
25
  groupStore: GroupStoreAdapter;
19
26
  transport?: TransportAdapter;
27
+ logLevel?: LogLevel;
20
28
  }
21
29
 
22
30
  /**
23
31
  * Main ChatSDK class - production-ready WhatsApp-style chat SDK
32
+ * Extends EventEmitter to provide event-driven architecture
33
+ *
34
+ * Events:
35
+ * - message:sent - Emitted when a message is sent
36
+ * - message:received - Emitted when a message is received
37
+ * - message:failed - Emitted when a message fails to send
38
+ * - connection:state - Emitted when connection state changes
39
+ * - session:created - Emitted when a session is created
40
+ * - group:created - Emitted when a group is created
41
+ * - user:created - Emitted when a user is created
42
+ * - error - Emitted when an error occurs
24
43
  */
25
- export class ChatSDK {
44
+ export class ChatSDK extends EventEmitter {
26
45
  private config: ChatSDKConfig;
27
46
  private currentUser: User | null = null;
47
+ private messageQueue: MessageQueue;
28
48
 
29
49
  constructor(config: ChatSDKConfig) {
50
+ super();
30
51
  this.config = config;
52
+ this.messageQueue = new MessageQueue();
53
+
54
+ // Set log level
55
+ if (config.logLevel !== undefined) {
56
+ logger.setLevel(config.logLevel);
57
+ }
58
+
59
+ // Set up transport event handlers if transport is provided
60
+ if (this.config.transport) {
61
+ this.setupTransportHandlers();
62
+ }
63
+
64
+ logger.info('ChatSDK initialized');
65
+ }
66
+
67
+ private setupTransportHandlers(): void {
68
+ if (!this.config.transport) return;
69
+
70
+ // Handle incoming messages
71
+ this.config.transport.onMessage((message: Message) => {
72
+ logger.debug('Message received via transport', { messageId: message.id });
73
+ this.emit(EVENTS.MESSAGE_RECEIVED, message);
74
+
75
+ // Store received message
76
+ this.config.messageStore.create(message).catch((error) => {
77
+ logger.error('Failed to store received message', error);
78
+ });
79
+ });
80
+
81
+ // Handle connection state changes
82
+ if (this.config.transport.onConnectionStateChange) {
83
+ this.config.transport.onConnectionStateChange((state: ConnectionState) => {
84
+ logger.info('Connection state changed', { state });
85
+ this.emit(EVENTS.CONNECTION_STATE_CHANGED, state);
86
+
87
+ // Process queued messages when reconnected
88
+ if (state === ConnectionState.CONNECTED) {
89
+ this.processMessageQueue();
90
+ }
91
+ });
92
+ }
93
+
94
+ // Handle transport errors
95
+ if (this.config.transport.onError) {
96
+ this.config.transport.onError((error: Error) => {
97
+ logger.error('Transport error', error);
98
+ this.emit(EVENTS.ERROR, error);
99
+ });
100
+ }
101
+ }
102
+
103
+ private async processMessageQueue(): Promise<void> {
104
+ const pending = this.messageQueue.getPendingMessages();
105
+ const retryable = this.messageQueue.getRetryableMessages();
106
+ const toSend = [...pending, ...retryable];
107
+
108
+ logger.info('Processing message queue', { count: toSend.length });
109
+
110
+ for (const queued of toSend) {
111
+ try {
112
+ await this.config.transport!.send(queued.message);
113
+ this.messageQueue.markSent(queued.message.id);
114
+ this.emit(EVENTS.MESSAGE_SENT, queued.message);
115
+ } catch (error) {
116
+ this.messageQueue.markFailed(
117
+ queued.message.id,
118
+ error instanceof Error ? error : new Error(String(error))
119
+ );
120
+ this.emit(EVENTS.MESSAGE_FAILED, queued.message, error);
121
+ }
122
+ }
31
123
  }
32
124
 
33
125
  /**
34
126
  * Create a new user with generated identity keys
35
127
  */
36
128
  async createUser(username: string): Promise<User> {
129
+ validateUsername(username);
130
+
37
131
  const keyPair = generateIdentityKeyPair();
38
132
  const user: User = {
39
133
  id: generateUUID(),
@@ -43,25 +137,63 @@ export class ChatSDK {
43
137
  privateKey: keyPair.privateKey,
44
138
  };
45
139
 
46
- await this.config.userStore.create(user);
47
- return user;
140
+ try {
141
+ await this.config.userStore.create(user);
142
+ logger.info('User created', { userId: user.id, username: user.username });
143
+ this.emit(EVENTS.USER_CREATED, user);
144
+ return user;
145
+ } catch (error) {
146
+ const storageError = new StorageError(
147
+ 'Failed to create user',
148
+ true,
149
+ { username, error: error instanceof Error ? error.message : String(error) }
150
+ );
151
+ logger.error('User creation failed', storageError);
152
+ this.emit(EVENTS.ERROR, storageError);
153
+ throw storageError;
154
+ }
48
155
  }
49
156
 
50
157
  /**
51
158
  * Import an existing user from stored data
52
159
  */
53
160
  async importUser(userData: StoredUser): Promise<User> {
54
- await this.config.userStore.save(userData);
55
- return userData;
161
+ try {
162
+ await this.config.userStore.save(userData);
163
+ logger.info('User imported', { userId: userData.id });
164
+ return userData;
165
+ } catch (error) {
166
+ const storageError = new StorageError(
167
+ 'Failed to import user',
168
+ true,
169
+ { userId: userData.id, error: error instanceof Error ? error.message : String(error) }
170
+ );
171
+ logger.error('User import failed', storageError);
172
+ this.emit(EVENTS.ERROR, storageError);
173
+ throw storageError;
174
+ }
56
175
  }
57
176
 
58
177
  /**
59
178
  * Set the current active user
60
179
  */
61
- setCurrentUser(user: User): void {
180
+ async setCurrentUser(user: User): Promise<void> {
62
181
  this.currentUser = user;
182
+ logger.info('Current user set', { userId: user.id, username: user.username });
183
+
63
184
  if (this.config.transport) {
64
- this.config.transport.connect(user.id);
185
+ try {
186
+ await this.config.transport.connect(user.id);
187
+ } catch (error) {
188
+ const transportError = new TransportError(
189
+ 'Failed to connect transport',
190
+ true,
191
+ { userId: user.id, error: error instanceof Error ? error.message : String(error) }
192
+ );
193
+ logger.error('Transport connection failed', transportError);
194
+ this.emit(EVENTS.ERROR, transportError);
195
+ throw transportError;
196
+ }
65
197
  }
66
198
  }
67
199
 
@@ -79,18 +211,30 @@ export class ChatSDK {
79
211
  // Create consistent session ID regardless of user order
80
212
  const ids = [userA.id, userB.id].sort();
81
213
  const sessionId = `${ids[0]}-${ids[1]}`;
82
- const session = new ChatSession(sessionId, userA, userB);
83
- await session.initialize();
84
- return session;
214
+
215
+ try {
216
+ const session = new ChatSession(sessionId, userA, userB);
217
+ await session.initialize();
218
+ logger.info('Chat session created', { sessionId, users: [userA.id, userB.id] });
219
+ this.emit(EVENTS.SESSION_CREATED, session);
220
+ return session;
221
+ } catch (error) {
222
+ const sessionError = new SessionError(
223
+ 'Failed to create chat session',
224
+ { sessionId, error: error instanceof Error ? error.message : String(error) }
225
+ );
226
+ logger.error('Session creation failed', sessionError);
227
+ this.emit(EVENTS.ERROR, sessionError);
228
+ throw sessionError;
229
+ }
85
230
  }
86
231
 
87
232
  /**
88
233
  * Create a new group with members
89
234
  */
90
235
  async createGroup(name: string, members: User[]): Promise<GroupSession> {
91
- if (members.length < 2) {
92
- throw new Error("Group must have at least 2 members");
93
- }
236
+ validateGroupName(name);
237
+ validateGroupMembers(members.length);
94
238
 
95
239
  const group: Group = {
96
240
  id: generateUUID(),
@@ -99,24 +243,53 @@ export class ChatSDK {
99
243
  createdAt: Date.now(),
100
244
  };
101
245
 
102
- await this.config.groupStore.create(group);
103
- const session = new GroupSession(group);
104
- await session.initialize();
105
- return session;
246
+ try {
247
+ await this.config.groupStore.create(group);
248
+ const session = new GroupSession(group);
249
+ await session.initialize();
250
+ logger.info('Group created', { groupId: group.id, name: group.name, memberCount: members.length });
251
+ this.emit(EVENTS.GROUP_CREATED, session);
252
+ return session;
253
+ } catch (error) {
254
+ const storageError = new StorageError(
255
+ 'Failed to create group',
256
+ true,
257
+ { groupName: name, error: error instanceof Error ? error.message : String(error) }
258
+ );
259
+ logger.error('Group creation failed', storageError);
260
+ this.emit(EVENTS.ERROR, storageError);
261
+ throw storageError;
262
+ }
106
263
  }
107
264
 
108
265
  /**
109
266
  * Load an existing group by ID
110
267
  */
111
268
  async loadGroup(id: string): Promise<GroupSession> {
112
- const group = await this.config.groupStore.findById(id);
113
- if (!group) {
114
- throw new Error(`Group not found: ${id}`);
115
- }
269
+ try {
270
+ const group = await this.config.groupStore.findById(id);
271
+ if (!group) {
272
+ throw new SessionError(`Group not found: ${id}`, { groupId: id });
273
+ }
116
274
 
117
- const session = new GroupSession(group);
118
- await session.initialize();
119
- return session;
275
+ const session = new GroupSession(group);
276
+ await session.initialize();
277
+ logger.debug('Group loaded', { groupId: id });
278
+ return session;
279
+ } catch (error) {
280
+ if (error instanceof SessionError) {
281
+ this.emit(EVENTS.ERROR, error);
282
+ throw error;
283
+ }
284
+ const storageError = new StorageError(
285
+ 'Failed to load group',
286
+ true,
287
+ { groupId: id, error: error instanceof Error ? error.message : String(error) }
288
+ );
289
+ logger.error('Group load failed', storageError);
290
+ this.emit(EVENTS.ERROR, storageError);
291
+ throw storageError;
292
+ }
120
293
  }
121
294
 
122
295
  /**
@@ -127,59 +300,201 @@ export class ChatSDK {
127
300
  plaintext: string
128
301
  ): Promise<Message> {
129
302
  if (!this.currentUser) {
130
- throw new Error("No current user set. Call setCurrentUser() first.");
303
+ throw new SessionError("No current user set. Call setCurrentUser() first.");
131
304
  }
132
305
 
306
+ validateMessage(plaintext);
307
+
133
308
  let message: Message;
134
- if (session instanceof ChatSession) {
135
- message = await session.encrypt(plaintext, this.currentUser.id);
136
- } else {
137
- message = await session.encrypt(plaintext, this.currentUser.id);
138
- }
309
+ try {
310
+ if (session instanceof ChatSession) {
311
+ message = await session.encrypt(plaintext, this.currentUser.id);
312
+ } else {
313
+ message = await session.encrypt(plaintext, this.currentUser.id);
314
+ }
139
315
 
140
- // Store the message
141
- await this.config.messageStore.create(message);
316
+ // Store the message
317
+ await this.config.messageStore.create(message);
318
+ logger.debug('Message stored', { messageId: message.id });
142
319
 
143
- // Send via transport if available
144
- if (this.config.transport) {
145
- await this.config.transport.send(message);
320
+ // Send via transport if available
321
+ if (this.config.transport) {
322
+ if (this.config.transport.isConnected()) {
323
+ try {
324
+ await this.config.transport.send(message);
325
+ logger.debug('Message sent via transport', { messageId: message.id });
326
+ this.emit(EVENTS.MESSAGE_SENT, message);
327
+ } catch (error) {
328
+ // Queue message for retry
329
+ this.messageQueue.enqueue(message);
330
+ this.messageQueue.markFailed(
331
+ message.id,
332
+ error instanceof Error ? error : new Error(String(error))
333
+ );
334
+ logger.warn('Message send failed, queued for retry', { messageId: message.id });
335
+ this.emit(EVENTS.MESSAGE_FAILED, message, error);
336
+ }
337
+ } else {
338
+ // Queue message if offline
339
+ this.messageQueue.enqueue(message);
340
+ logger.info('Message queued (offline)', { messageId: message.id });
341
+ }
342
+ }
343
+
344
+ return message;
345
+ } catch (error) {
346
+ const sendError = error instanceof Error ? error : new Error(String(error));
347
+ logger.error('Failed to send message', sendError);
348
+ this.emit(EVENTS.ERROR, sendError);
349
+ throw sendError;
146
350
  }
351
+ }
147
352
 
148
- return message;
353
+ /**
354
+ * Send a media message in a chat session (1:1 or group)
355
+ */
356
+ async sendMediaMessage(
357
+ session: ChatSession | GroupSession,
358
+ caption: string,
359
+ media: MediaAttachment
360
+ ): Promise<Message> {
361
+ if (!this.currentUser) {
362
+ throw new SessionError("No current user set. Call setCurrentUser() first.");
363
+ }
364
+
365
+ let message: Message;
366
+ try {
367
+ if (session instanceof ChatSession) {
368
+ message = await session.encryptMedia(caption, media, this.currentUser.id);
369
+ } else {
370
+ message = await session.encryptMedia(caption, media, this.currentUser.id);
371
+ }
372
+
373
+ // Store the message
374
+ await this.config.messageStore.create(message);
375
+ logger.debug('Media message stored', { messageId: message.id, mediaType: media.type });
376
+
377
+ // Send via transport if available
378
+ if (this.config.transport) {
379
+ if (this.config.transport.isConnected()) {
380
+ try {
381
+ await this.config.transport.send(message);
382
+ logger.debug('Media message sent via transport', { messageId: message.id });
383
+ this.emit(EVENTS.MESSAGE_SENT, message);
384
+ } catch (error) {
385
+ // Queue message for retry
386
+ this.messageQueue.enqueue(message);
387
+ this.messageQueue.markFailed(
388
+ message.id,
389
+ error instanceof Error ? error : new Error(String(error))
390
+ );
391
+ logger.warn('Media message send failed, queued for retry', { messageId: message.id });
392
+ this.emit(EVENTS.MESSAGE_FAILED, message, error);
393
+ }
394
+ } else {
395
+ // Queue message if offline
396
+ this.messageQueue.enqueue(message);
397
+ logger.info('Media message queued (offline)', { messageId: message.id });
398
+ }
399
+ }
400
+
401
+ return message;
402
+ } catch (error) {
403
+ const sendError = error instanceof Error ? error : new Error(String(error));
404
+ logger.error('Failed to send media message', sendError);
405
+ this.emit(EVENTS.ERROR, sendError);
406
+ throw sendError;
407
+ }
149
408
  }
150
409
 
151
410
  /**
152
411
  * Decrypt a message
153
412
  */
154
413
  async decryptMessage(message: Message, user: User): Promise<string> {
155
- if (message.groupId) {
156
- // Group message
157
- const group = await this.config.groupStore.findById(message.groupId);
158
- if (!group) {
159
- throw new Error(`Group not found: ${message.groupId}`);
160
- }
161
- const session = new GroupSession(group);
162
- await session.initialize();
163
- return await session.decrypt(message);
164
- } else {
165
- // 1:1 message - need to find the session
166
- const otherUserId =
167
- message.senderId === user.id ? message.receiverId : message.senderId;
168
- if (!otherUserId) {
169
- throw new Error("Invalid message: missing receiver/sender");
170
- }
414
+ try {
415
+ if (message.groupId) {
416
+ // Group message
417
+ const group = await this.config.groupStore.findById(message.groupId);
418
+ if (!group) {
419
+ throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
420
+ }
421
+ const session = new GroupSession(group);
422
+ await session.initialize();
423
+ return await session.decrypt(message);
424
+ } else {
425
+ // 1:1 message - need to find the session
426
+ const otherUserId =
427
+ message.senderId === user.id ? message.receiverId : message.senderId;
428
+ if (!otherUserId) {
429
+ throw new SessionError("Invalid message: missing receiver/sender");
430
+ }
171
431
 
172
- const otherUser = await this.config.userStore.findById(otherUserId);
173
- if (!otherUser) {
174
- throw new Error(`User not found: ${otherUserId}`);
432
+ const otherUser = await this.config.userStore.findById(otherUserId);
433
+ if (!otherUser) {
434
+ throw new SessionError(`User not found: ${otherUserId}`, { userId: otherUserId });
435
+ }
436
+
437
+ // Create consistent session ID
438
+ const ids = [user.id, otherUser.id].sort();
439
+ const sessionId = `${ids[0]}-${ids[1]}`;
440
+ const session = new ChatSession(sessionId, user, otherUser);
441
+ await session.initializeForUser(user);
442
+ return await session.decrypt(message, user);
175
443
  }
444
+ } catch (error) {
445
+ const decryptError = error instanceof Error ? error : new Error(String(error));
446
+ logger.error('Failed to decrypt message', decryptError);
447
+ this.emit(EVENTS.ERROR, decryptError);
448
+ throw decryptError;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Decrypt a media message
454
+ */
455
+ async decryptMediaMessage(
456
+ message: Message,
457
+ user: User
458
+ ): Promise<{ text: string; media: MediaAttachment }> {
459
+ if (!message.media) {
460
+ throw new SessionError("Message does not contain media");
461
+ }
462
+
463
+ try {
464
+ if (message.groupId) {
465
+ // Group media message
466
+ const group = await this.config.groupStore.findById(message.groupId);
467
+ if (!group) {
468
+ throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
469
+ }
470
+ const session = new GroupSession(group);
471
+ await session.initialize();
472
+ return await session.decryptMedia(message);
473
+ } else {
474
+ // 1:1 media message
475
+ const otherUserId =
476
+ message.senderId === user.id ? message.receiverId : message.senderId;
477
+ if (!otherUserId) {
478
+ throw new SessionError("Invalid message: missing receiver/sender");
479
+ }
176
480
 
177
- // Create consistent session ID
178
- const ids = [user.id, otherUser.id].sort();
179
- const sessionId = `${ids[0]}-${ids[1]}`;
180
- const session = new ChatSession(sessionId, user, otherUser);
181
- await session.initializeForUser(user);
182
- return await session.decrypt(message, user);
481
+ const otherUser = await this.config.userStore.findById(otherUserId);
482
+ if (!otherUser) {
483
+ throw new SessionError(`User not found: ${otherUserId}`, { userId: otherUserId });
484
+ }
485
+
486
+ // Create consistent session ID
487
+ const ids = [user.id, otherUser.id].sort();
488
+ const sessionId = `${ids[0]}-${ids[1]}`;
489
+ const session = new ChatSession(sessionId, user, otherUser);
490
+ await session.initializeForUser(user);
491
+ return await session.decryptMedia(message, user);
492
+ }
493
+ } catch (error) {
494
+ const decryptError = error instanceof Error ? error : new Error(String(error));
495
+ logger.error('Failed to decrypt media message', decryptError);
496
+ this.emit(EVENTS.ERROR, decryptError);
497
+ throw decryptError;
183
498
  }
184
499
  }
185
500
 
@@ -187,14 +502,154 @@ export class ChatSDK {
187
502
  * Get messages for a user
188
503
  */
189
504
  async getMessagesForUser(userId: string): Promise<Message[]> {
190
- return await this.config.messageStore.listByUser(userId);
505
+ try {
506
+ return await this.config.messageStore.listByUser(userId);
507
+ } catch (error) {
508
+ const storageError = new StorageError(
509
+ 'Failed to get messages for user',
510
+ true,
511
+ { userId, error: error instanceof Error ? error.message : String(error) }
512
+ );
513
+ logger.error('Get messages failed', storageError);
514
+ this.emit(EVENTS.ERROR, storageError);
515
+ throw storageError;
516
+ }
191
517
  }
192
518
 
193
519
  /**
194
520
  * Get messages for a group
195
521
  */
196
522
  async getMessagesForGroup(groupId: string): Promise<Message[]> {
197
- return await this.config.messageStore.listByGroup(groupId);
523
+ try {
524
+ return await this.config.messageStore.listByGroup(groupId);
525
+ } catch (error) {
526
+ const storageError = new StorageError(
527
+ 'Failed to get messages for group',
528
+ true,
529
+ { groupId, error: error instanceof Error ? error.message : String(error) }
530
+ );
531
+ logger.error('Get messages failed', storageError);
532
+ this.emit(EVENTS.ERROR, storageError);
533
+ throw storageError;
534
+ }
535
+ }
536
+
537
+ // ========== Public Accessor Methods ==========
538
+
539
+ /**
540
+ * Get the transport adapter
541
+ */
542
+ getTransport(): TransportAdapter | undefined {
543
+ return this.config.transport;
544
+ }
545
+
546
+ /**
547
+ * Get all users
548
+ */
549
+ async listUsers(): Promise<User[]> {
550
+ try {
551
+ return await this.config.userStore.list();
552
+ } catch (error) {
553
+ const storageError = new StorageError(
554
+ 'Failed to list users',
555
+ true,
556
+ { error: error instanceof Error ? error.message : String(error) }
557
+ );
558
+ logger.error('List users failed', storageError);
559
+ this.emit(EVENTS.ERROR, storageError);
560
+ throw storageError;
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Get user by ID
566
+ */
567
+ async getUserById(userId: string): Promise<User | undefined> {
568
+ try {
569
+ return await this.config.userStore.findById(userId);
570
+ } catch (error) {
571
+ const storageError = new StorageError(
572
+ 'Failed to get user',
573
+ true,
574
+ { userId, error: error instanceof Error ? error.message : String(error) }
575
+ );
576
+ logger.error('Get user failed', storageError);
577
+ this.emit(EVENTS.ERROR, storageError);
578
+ throw storageError;
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Get all groups
584
+ */
585
+ async listGroups(): Promise<Group[]> {
586
+ try {
587
+ return await this.config.groupStore.list();
588
+ } catch (error) {
589
+ const storageError = new StorageError(
590
+ 'Failed to list groups',
591
+ true,
592
+ { error: error instanceof Error ? error.message : String(error) }
593
+ );
594
+ logger.error('List groups failed', storageError);
595
+ this.emit(EVENTS.ERROR, storageError);
596
+ throw storageError;
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Get connection state
602
+ */
603
+ getConnectionState(): ConnectionState {
604
+ if (!this.config.transport) {
605
+ return ConnectionState.DISCONNECTED;
606
+ }
607
+ return this.config.transport.getConnectionState();
608
+ }
609
+
610
+ /**
611
+ * Check if connected
612
+ */
613
+ isConnected(): boolean {
614
+ if (!this.config.transport) {
615
+ return false;
616
+ }
617
+ return this.config.transport.isConnected();
618
+ }
619
+
620
+ /**
621
+ * Disconnect transport
622
+ */
623
+ async disconnect(): Promise<void> {
624
+ if (this.config.transport) {
625
+ await this.config.transport.disconnect();
626
+ logger.info('Transport disconnected');
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Reconnect transport
632
+ */
633
+ async reconnect(): Promise<void> {
634
+ if (this.config.transport) {
635
+ await this.config.transport.reconnect();
636
+ logger.info('Transport reconnected');
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Get message queue status
642
+ */
643
+ getQueueStatus(): {
644
+ size: number;
645
+ pending: number;
646
+ retryable: number;
647
+ } {
648
+ return {
649
+ size: this.messageQueue.size(),
650
+ pending: this.messageQueue.getPendingMessages().length,
651
+ retryable: this.messageQueue.getRetryableMessages().length,
652
+ };
198
653
  }
199
654
  }
200
655
 
@@ -205,8 +660,15 @@ export * from "./stores/memory/messageStore.js";
205
660
  export * from "./stores/memory/groupStore.js";
206
661
  export * from "./transport/adapters.js";
207
662
  export * from "./transport/memoryTransport.js";
663
+ export * from "./transport/websocketClient.js";
208
664
  export * from "./models/user.js";
209
665
  export * from "./models/message.js";
210
666
  export * from "./models/group.js";
667
+ export * from "./models/mediaTypes.js";
211
668
  export * from "./chat/ChatSession.js";
212
669
  export * from "./chat/GroupSession.js";
670
+ export * from "./utils/errors.js";
671
+ export * from "./utils/logger.js";
672
+ export * from "./utils/validation.js";
673
+ export * from "./utils/mediaUtils.js";
674
+ export * from "./constants.js";