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
@@ -1,37 +1,285 @@
1
+ import { Message } from "../models/message.js";
2
+ import { TransportAdapter } from "./adapters.js";
3
+ import {
4
+ ConnectionState,
5
+ RECONNECT_MAX_ATTEMPTS,
6
+ RECONNECT_BASE_DELAY,
7
+ RECONNECT_MAX_DELAY,
8
+ HEARTBEAT_INTERVAL,
9
+ CONNECTION_TIMEOUT,
10
+ } from "../constants.js";
11
+ import { NetworkError, TransportError } from "../utils/errors.js";
12
+ import { logger } from "../utils/logger.js";
1
13
 
2
- import WebSocket from 'ws';
3
-
4
- export class ChatClient {
14
+ export class WebSocketClient implements TransportAdapter {
5
15
  private ws: WebSocket | null = null;
6
- private messageHandlers: ((message: Buffer) => void)[] = [];
16
+ private url: string;
17
+ private messageHandler: ((message: Message) => void) | null = null;
18
+ private stateHandler: ((state: ConnectionState) => void) | null = null;
19
+ private errorHandler: ((error: Error) => void) | null = null;
20
+ private connectionState: ConnectionState = ConnectionState.DISCONNECTED;
21
+ private reconnectAttempts: number = 0;
22
+ private reconnectTimer: NodeJS.Timeout | null = null;
23
+ private heartbeatTimer: NodeJS.Timeout | null = null;
24
+ private currentUserId: string | null = null;
25
+ private shouldReconnect: boolean = true;
7
26
 
8
- constructor(private url: string) {}
27
+ constructor(url: string) {
28
+ this.url = url;
29
+ }
9
30
 
10
- connect(): void {
11
- this.ws = new WebSocket(this.url);
31
+ async connect(userId: string): Promise<void> {
32
+ this.currentUserId = userId;
33
+ this.shouldReconnect = true;
34
+ return this.doConnect();
35
+ }
12
36
 
13
- this.ws.on('open', () => {
14
- console.log('Connected to WebSocket server');
15
- });
37
+ private async doConnect(): Promise<void> {
38
+ if (this.connectionState === ConnectionState.CONNECTING) {
39
+ logger.warn('Already connecting, skipping duplicate connect attempt');
40
+ return;
41
+ }
16
42
 
17
- this.ws.on('message', (message: Buffer) => {
18
- this.messageHandlers.forEach(handler => handler(message));
19
- });
43
+ this.updateState(ConnectionState.CONNECTING);
44
+ logger.info('Connecting to WebSocket', { url: this.url, userId: this.currentUserId });
45
+
46
+ return new Promise((resolve, reject) => {
47
+ try {
48
+ const wsUrl = this.currentUserId
49
+ ? `${this.url}?userId=${this.currentUserId}`
50
+ : this.url;
51
+
52
+ this.ws = new WebSocket(wsUrl);
53
+
54
+ const connectionTimeout = setTimeout(() => {
55
+ if (this.connectionState === ConnectionState.CONNECTING) {
56
+ this.ws?.close();
57
+ const error = new NetworkError('Connection timeout');
58
+ this.handleError(error);
59
+ reject(error);
60
+ }
61
+ }, CONNECTION_TIMEOUT);
62
+
63
+ this.ws.onopen = () => {
64
+ clearTimeout(connectionTimeout);
65
+ this.reconnectAttempts = 0;
66
+ this.updateState(ConnectionState.CONNECTED);
67
+ logger.info('WebSocket connected');
68
+ this.startHeartbeat();
69
+ resolve();
70
+ };
71
+
72
+ this.ws.onmessage = (event) => {
73
+ try {
74
+ const message = JSON.parse(event.data);
75
+
76
+ // Handle pong response
77
+ if (message.type === 'pong') {
78
+ logger.debug('Received pong');
79
+ return;
80
+ }
81
+
82
+ if (this.messageHandler) {
83
+ this.messageHandler(message);
84
+ }
85
+ } catch (error) {
86
+ const parseError = new TransportError(
87
+ 'Failed to parse message',
88
+ false,
89
+ { error: error instanceof Error ? error.message : String(error) }
90
+ );
91
+ logger.error('Message parse error', parseError);
92
+ this.handleError(parseError);
93
+ }
94
+ };
95
+
96
+ this.ws.onerror = (event) => {
97
+ clearTimeout(connectionTimeout);
98
+ const error = new NetworkError('WebSocket error', {
99
+ event: event.type,
100
+ });
101
+ logger.error('WebSocket error', error);
102
+ this.handleError(error);
103
+ reject(error);
104
+ };
105
+
106
+ this.ws.onclose = (event) => {
107
+ clearTimeout(connectionTimeout);
108
+ this.stopHeartbeat();
109
+ logger.info('WebSocket closed', {
110
+ code: event.code,
111
+ reason: event.reason,
112
+ wasClean: event.wasClean,
113
+ });
20
114
 
21
- this.ws.on('close', () => {
22
- console.log('Disconnected from WebSocket server');
115
+ if (this.connectionState !== ConnectionState.DISCONNECTED) {
116
+ this.updateState(ConnectionState.DISCONNECTED);
117
+
118
+ // Attempt reconnection if not manually disconnected
119
+ if (this.shouldReconnect && this.reconnectAttempts < RECONNECT_MAX_ATTEMPTS) {
120
+ this.scheduleReconnect();
121
+ } else if (this.reconnectAttempts >= RECONNECT_MAX_ATTEMPTS) {
122
+ this.updateState(ConnectionState.FAILED);
123
+ const error = new NetworkError('Max reconnection attempts exceeded');
124
+ this.handleError(error);
125
+ }
126
+ }
127
+ };
128
+ } catch (error) {
129
+ const connectError = new NetworkError(
130
+ 'Failed to create WebSocket connection',
131
+ { error: error instanceof Error ? error.message : String(error) }
132
+ );
133
+ logger.error('Connection error', connectError);
134
+ this.handleError(connectError);
135
+ reject(connectError);
136
+ }
23
137
  });
24
138
  }
25
139
 
26
- sendMessage(message: string | Buffer): void {
27
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
28
- this.ws.send(message);
140
+ async disconnect(): Promise<void> {
141
+ logger.info('Disconnecting WebSocket');
142
+ this.shouldReconnect = false;
143
+ this.clearReconnectTimer();
144
+ this.stopHeartbeat();
145
+
146
+ if (this.ws) {
147
+ this.ws.close(1000, 'Client disconnect');
148
+ this.ws = null;
149
+ }
150
+
151
+ this.updateState(ConnectionState.DISCONNECTED);
152
+ }
153
+
154
+ async reconnect(): Promise<void> {
155
+ logger.info('Manual reconnect requested');
156
+ this.reconnectAttempts = 0;
157
+ this.shouldReconnect = true;
158
+ await this.disconnect();
159
+
160
+ if (this.currentUserId) {
161
+ await this.doConnect();
29
162
  } else {
30
- console.error('WebSocket is not connected.');
163
+ throw new TransportError('Cannot reconnect: no user ID set');
31
164
  }
32
165
  }
33
166
 
34
- onMessage(handler: (message: Buffer) => void): void {
35
- this.messageHandlers.push(handler);
167
+ private scheduleReconnect(): void {
168
+ this.clearReconnectTimer();
169
+ this.reconnectAttempts++;
170
+
171
+ // Exponential backoff with jitter
172
+ const delay = Math.min(
173
+ RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1),
174
+ RECONNECT_MAX_DELAY
175
+ );
176
+ const jitter = Math.random() * 1000;
177
+ const totalDelay = delay + jitter;
178
+
179
+ logger.info('Scheduling reconnect', {
180
+ attempt: this.reconnectAttempts,
181
+ delay: totalDelay,
182
+ });
183
+
184
+ this.updateState(ConnectionState.RECONNECTING);
185
+
186
+ this.reconnectTimer = setTimeout(async () => {
187
+ try {
188
+ await this.doConnect();
189
+ } catch (error) {
190
+ logger.error('Reconnect failed', error);
191
+ }
192
+ }, totalDelay);
193
+ }
194
+
195
+ private clearReconnectTimer(): void {
196
+ if (this.reconnectTimer) {
197
+ clearTimeout(this.reconnectTimer);
198
+ this.reconnectTimer = null;
199
+ }
200
+ }
201
+
202
+ private startHeartbeat(): void {
203
+ this.stopHeartbeat();
204
+
205
+ this.heartbeatTimer = setInterval(() => {
206
+ if (this.isConnected()) {
207
+ try {
208
+ this.ws?.send(JSON.stringify({ type: 'ping' }));
209
+ logger.debug('Sent ping');
210
+ } catch (error) {
211
+ logger.error('Failed to send heartbeat', error);
212
+ }
213
+ }
214
+ }, HEARTBEAT_INTERVAL);
215
+ }
216
+
217
+ private stopHeartbeat(): void {
218
+ if (this.heartbeatTimer) {
219
+ clearInterval(this.heartbeatTimer);
220
+ this.heartbeatTimer = null;
221
+ }
222
+ }
223
+
224
+ async send(message: Message): Promise<void> {
225
+ if (!this.isConnected() || !this.ws) {
226
+ throw new NetworkError('WebSocket not connected');
227
+ }
228
+
229
+ try {
230
+ this.ws.send(JSON.stringify(message));
231
+ logger.debug('Message sent', { messageId: message.id });
232
+ } catch (error) {
233
+ const sendError = new NetworkError(
234
+ 'Failed to send message',
235
+ { error: error instanceof Error ? error.message : String(error) }
236
+ );
237
+ logger.error('Send error', sendError);
238
+ throw sendError;
239
+ }
240
+ }
241
+
242
+ onMessage(handler: (message: Message) => void): void {
243
+ this.messageHandler = handler;
244
+ }
245
+
246
+ onConnectionStateChange(handler: (state: ConnectionState) => void): void {
247
+ this.stateHandler = handler;
248
+ }
249
+
250
+ onError(handler: (error: Error) => void): void {
251
+ this.errorHandler = handler;
252
+ }
253
+
254
+ getConnectionState(): ConnectionState {
255
+ return this.connectionState;
256
+ }
257
+
258
+ isConnected(): boolean {
259
+ return (
260
+ this.connectionState === ConnectionState.CONNECTED &&
261
+ this.ws?.readyState === WebSocket.OPEN
262
+ );
263
+ }
264
+
265
+ private updateState(newState: ConnectionState): void {
266
+ if (this.connectionState !== newState) {
267
+ const oldState = this.connectionState;
268
+ this.connectionState = newState;
269
+ logger.info('Connection state changed', {
270
+ from: oldState,
271
+ to: newState,
272
+ });
273
+
274
+ if (this.stateHandler) {
275
+ this.stateHandler(newState);
276
+ }
277
+ }
278
+ }
279
+
280
+ private handleError(error: Error): void {
281
+ if (this.errorHandler) {
282
+ this.errorHandler(error);
283
+ }
36
284
  }
37
285
  }
@@ -1,33 +1,33 @@
1
1
 
2
- import { WebSocketServer, WebSocket } from 'ws';
2
+ // import { WebSocketServer, WebSocket } from 'ws';
3
3
 
4
- export class ChatServer {
5
- private wss: WebSocketServer;
4
+ // export class ChatServer {
5
+ // private wss: WebSocketServer;
6
6
 
7
- constructor(port: number) {
8
- this.wss = new WebSocketServer({ port });
9
- this.initialize();
10
- }
7
+ // constructor(port: number) {
8
+ // this.wss = new WebSocketServer({ port });
9
+ // this.initialize();
10
+ // }
11
11
 
12
- private initialize(): void {
13
- this.wss.on('connection', (ws: WebSocket) => {
14
- console.log('Client connected');
12
+ // private initialize(): void {
13
+ // this.wss.on('connection', (ws: WebSocket) => {
14
+ // console.log('Client connected');
15
15
 
16
- ws.on('message', (message: Buffer) => {
17
- console.log('Received message:', message.toString());
18
- // Broadcast the message to all other clients
19
- this.wss.clients.forEach((client) => {
20
- if (client !== ws && client.readyState === WebSocket.OPEN) {
21
- client.send(message);
22
- }
23
- });
24
- });
16
+ // ws.on('message', (message: Buffer) => {
17
+ // console.log('Received message:', message.toString());
18
+ // // Broadcast the message to all other clients
19
+ // this.wss.clients.forEach((client) => {
20
+ // if (client !== ws && client.readyState === WebSocket.OPEN) {
21
+ // client.send(message);
22
+ // }
23
+ // });
24
+ // });
25
25
 
26
- ws.on('close', () => {
27
- console.log('Client disconnected');
28
- });
29
- });
26
+ // ws.on('close', () => {
27
+ // console.log('Client disconnected');
28
+ // });
29
+ // });
30
30
 
31
- console.log(`WebSocket server started on port ${this.wss.options.port}`);
32
- }
33
- }
31
+ // console.log(`WebSocket server started on port ${this.wss.options.port}`);
32
+ // }
33
+ // }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Base SDK Error class
3
+ */
4
+ export class SDKError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public readonly code: string,
8
+ public readonly retryable: boolean = false,
9
+ public readonly details?: Record<string, unknown>
10
+ ) {
11
+ super(message);
12
+ this.name = this.constructor.name;
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
15
+
16
+ toJSON() {
17
+ return {
18
+ name: this.name,
19
+ message: this.message,
20
+ code: this.code,
21
+ retryable: this.retryable,
22
+ details: this.details,
23
+ };
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Network-related errors (connection, timeout, etc.)
29
+ */
30
+ export class NetworkError extends SDKError {
31
+ constructor(message: string, details?: Record<string, unknown>) {
32
+ super(message, 'NETWORK_ERROR', true, details);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Encryption/Decryption errors
38
+ */
39
+ export class EncryptionError extends SDKError {
40
+ constructor(message: string, details?: Record<string, unknown>) {
41
+ super(message, 'ENCRYPTION_ERROR', false, details);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Authentication/Authorization errors
47
+ */
48
+ export class AuthError extends SDKError {
49
+ constructor(message: string, details?: Record<string, unknown>) {
50
+ super(message, 'AUTH_ERROR', false, details);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Validation errors (invalid input)
56
+ */
57
+ export class ValidationError extends SDKError {
58
+ constructor(message: string, details?: Record<string, unknown>) {
59
+ super(message, 'VALIDATION_ERROR', false, details);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Storage-related errors
65
+ */
66
+ export class StorageError extends SDKError {
67
+ constructor(message: string, retryable: boolean = true, details?: Record<string, unknown>) {
68
+ super(message, 'STORAGE_ERROR', retryable, details);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Session-related errors
74
+ */
75
+ export class SessionError extends SDKError {
76
+ constructor(message: string, details?: Record<string, unknown>) {
77
+ super(message, 'SESSION_ERROR', false, details);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Transport-related errors
83
+ */
84
+ export class TransportError extends SDKError {
85
+ constructor(message: string, retryable: boolean = true, details?: Record<string, unknown>) {
86
+ super(message, 'TRANSPORT_ERROR', retryable, details);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Configuration errors
92
+ */
93
+ export class ConfigError extends SDKError {
94
+ constructor(message: string, details?: Record<string, unknown>) {
95
+ super(message, 'CONFIG_ERROR', false, details);
96
+ }
97
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Log levels
3
+ */
4
+ export enum LogLevel {
5
+ DEBUG = 0,
6
+ INFO = 1,
7
+ WARN = 2,
8
+ ERROR = 3,
9
+ NONE = 4,
10
+ }
11
+
12
+ /**
13
+ * Logger configuration
14
+ */
15
+ export interface LoggerConfig {
16
+ level: LogLevel;
17
+ prefix?: string;
18
+ timestamp?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Simple structured logger
23
+ */
24
+ export class Logger {
25
+ private config: LoggerConfig;
26
+
27
+ constructor(config: Partial<LoggerConfig> = {}) {
28
+ this.config = {
29
+ level: config.level ?? LogLevel.INFO,
30
+ prefix: config.prefix ?? '[ChatSDK]',
31
+ timestamp: config.timestamp ?? true,
32
+ };
33
+ }
34
+
35
+ private shouldLog(level: LogLevel): boolean {
36
+ return level >= this.config.level;
37
+ }
38
+
39
+ private formatMessage(level: string, message: string, data?: unknown): string {
40
+ const parts: string[] = [];
41
+
42
+ if (this.config.timestamp) {
43
+ parts.push(new Date().toISOString());
44
+ }
45
+
46
+ parts.push(this.config.prefix!);
47
+ parts.push(`[${level}]`);
48
+ parts.push(message);
49
+
50
+ let formatted = parts.join(' ');
51
+
52
+ if (data !== undefined) {
53
+ formatted += ' ' + JSON.stringify(data, null, 2);
54
+ }
55
+
56
+ return formatted;
57
+ }
58
+
59
+ debug(message: string, data?: unknown): void {
60
+ if (this.shouldLog(LogLevel.DEBUG)) {
61
+ console.debug(this.formatMessage('DEBUG', message, data));
62
+ }
63
+ }
64
+
65
+ info(message: string, data?: unknown): void {
66
+ if (this.shouldLog(LogLevel.INFO)) {
67
+ console.info(this.formatMessage('INFO', message, data));
68
+ }
69
+ }
70
+
71
+ warn(message: string, data?: unknown): void {
72
+ if (this.shouldLog(LogLevel.WARN)) {
73
+ console.warn(this.formatMessage('WARN', message, data));
74
+ }
75
+ }
76
+
77
+ error(message: string, error?: Error | unknown): void {
78
+ if (this.shouldLog(LogLevel.ERROR)) {
79
+ const errorData = error instanceof Error
80
+ ? { message: error.message, stack: error.stack }
81
+ : error;
82
+ console.error(this.formatMessage('ERROR', message, errorData));
83
+ }
84
+ }
85
+
86
+ setLevel(level: LogLevel): void {
87
+ this.config.level = level;
88
+ }
89
+
90
+ getLevel(): LogLevel {
91
+ return this.config.level;
92
+ }
93
+ }
94
+
95
+ // Default logger instance
96
+ export const logger = new Logger();