chatly-sdk 1.0.0 → 2.0.0

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