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.
- package/CONTRIBUTING.md +658 -0
- package/IMPROVEMENTS.md +402 -0
- package/LICENSE +21 -0
- package/README.md +1576 -162
- package/dist/index.d.ts +502 -11
- package/dist/index.js +1619 -66
- package/examples/01-basic-chat/README.md +61 -0
- package/examples/01-basic-chat/index.js +58 -0
- package/examples/01-basic-chat/package.json +13 -0
- package/examples/02-group-chat/README.md +78 -0
- package/examples/02-group-chat/index.js +76 -0
- package/examples/02-group-chat/package.json +13 -0
- package/examples/03-offline-messaging/README.md +73 -0
- package/examples/03-offline-messaging/index.js +80 -0
- package/examples/03-offline-messaging/package.json +13 -0
- package/examples/04-live-chat/README.md +80 -0
- package/examples/04-live-chat/index.js +114 -0
- package/examples/04-live-chat/package.json +13 -0
- package/examples/05-hybrid-messaging/README.md +71 -0
- package/examples/05-hybrid-messaging/index.js +106 -0
- package/examples/05-hybrid-messaging/package.json +13 -0
- package/examples/06-postgresql-integration/README.md +101 -0
- package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
- package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
- package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
- package/examples/06-postgresql-integration/index.js +92 -0
- package/examples/06-postgresql-integration/package.json +14 -0
- package/examples/06-postgresql-integration/schema.sql +58 -0
- package/examples/08-customer-support/README.md +70 -0
- package/examples/08-customer-support/index.js +104 -0
- package/examples/08-customer-support/package.json +13 -0
- package/examples/README.md +105 -0
- package/jest.config.cjs +28 -0
- package/package.json +15 -6
- package/src/chat/ChatSession.ts +160 -3
- package/src/chat/GroupSession.ts +108 -1
- package/src/constants.ts +61 -0
- package/src/crypto/e2e.ts +9 -20
- package/src/crypto/utils.ts +3 -1
- package/src/index.ts +530 -63
- package/src/models/mediaTypes.ts +62 -0
- package/src/models/message.ts +4 -1
- package/src/storage/adapters.ts +36 -0
- package/src/storage/localStorage.ts +49 -0
- package/src/storage/s3Storage.ts +84 -0
- package/src/stores/adapters.ts +2 -0
- package/src/stores/memory/messageStore.ts +8 -0
- package/src/transport/adapters.ts +51 -1
- package/src/transport/memoryTransport.ts +75 -13
- package/src/transport/websocketClient.ts +269 -21
- package/src/transport/websocketServer.ts +26 -26
- package/src/utils/errors.ts +97 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/mediaUtils.ts +235 -0
- package/src/utils/messageQueue.ts +162 -0
- package/src/utils/validation.ts +99 -0
- package/test/crypto.test.ts +122 -35
- package/test/sdk.test.ts +276 -0
- package/test/validation.test.ts +64 -0
- package/tsconfig.json +11 -10
- package/tsconfig.test.json +11 -0
- package/src/ChatManager.ts +0 -103
- 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
|
-
|
|
3
|
-
|
|
4
|
-
export class ChatClient {
|
|
14
|
+
export class WebSocketClient implements TransportAdapter {
|
|
5
15
|
private ws: WebSocket | null = null;
|
|
6
|
-
private
|
|
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(
|
|
27
|
+
constructor(url: string) {
|
|
28
|
+
this.url = url;
|
|
29
|
+
}
|
|
9
30
|
|
|
10
|
-
connect(): void {
|
|
11
|
-
this.
|
|
31
|
+
async connect(userId: string): Promise<void> {
|
|
32
|
+
this.currentUserId = userId;
|
|
33
|
+
this.shouldReconnect = true;
|
|
34
|
+
return this.doConnect();
|
|
35
|
+
}
|
|
12
36
|
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
163
|
+
throw new TransportError('Cannot reconnect: no user ID set');
|
|
31
164
|
}
|
|
32
165
|
}
|
|
33
166
|
|
|
34
|
-
|
|
35
|
-
this.
|
|
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
|
-
|
|
4
|
+
// export class ChatServer {
|
|
5
|
+
// private wss: WebSocketServer;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
// constructor(port: number) {
|
|
8
|
+
// this.wss = new WebSocketServer({ port });
|
|
9
|
+
// this.initialize();
|
|
10
|
+
// }
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// private initialize(): void {
|
|
13
|
+
// this.wss.on('connection', (ws: WebSocket) => {
|
|
14
|
+
// console.log('Client connected');
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// ws.on('close', () => {
|
|
27
|
+
// console.log('Client disconnected');
|
|
28
|
+
// });
|
|
29
|
+
// });
|
|
30
30
|
|
|
31
|
-
|
|
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();
|