chatly-sdk 0.0.4 → 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.
- package/CONTRIBUTING.md +658 -0
- package/IMPROVEMENTS.md +402 -0
- package/README.md +1539 -162
- package/dist/index.d.ts +430 -9
- package/dist/index.js +1420 -63
- 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 +12 -8
- package/src/chat/ChatSession.ts +81 -0
- package/src/chat/GroupSession.ts +79 -0
- package/src/constants.ts +61 -0
- package/src/crypto/e2e.ts +0 -20
- package/src/index.ts +525 -63
- package/src/models/mediaTypes.ts +58 -0
- package/src/models/message.ts +4 -1
- 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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media types supported by the SDK
|
|
3
|
+
*/
|
|
4
|
+
export enum MediaType {
|
|
5
|
+
IMAGE = 'image',
|
|
6
|
+
AUDIO = 'audio',
|
|
7
|
+
VIDEO = 'video',
|
|
8
|
+
DOCUMENT = 'document',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Media metadata
|
|
13
|
+
*/
|
|
14
|
+
export interface MediaMetadata {
|
|
15
|
+
filename: string;
|
|
16
|
+
mimeType: string;
|
|
17
|
+
size: number;
|
|
18
|
+
width?: number; // For images/videos
|
|
19
|
+
height?: number; // For images/videos
|
|
20
|
+
duration?: number; // For audio/video (in seconds)
|
|
21
|
+
thumbnail?: string; // Base64 thumbnail for images/videos
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Media attachment
|
|
26
|
+
*/
|
|
27
|
+
export interface MediaAttachment {
|
|
28
|
+
type: MediaType;
|
|
29
|
+
data: string; // Base64 encoded file data
|
|
30
|
+
metadata: MediaMetadata;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Supported MIME types
|
|
35
|
+
*/
|
|
36
|
+
export const SUPPORTED_MIME_TYPES = {
|
|
37
|
+
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|
38
|
+
audio: ['audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/wav', 'audio/webm'],
|
|
39
|
+
video: ['video/mp4', 'video/webm', 'video/ogg'],
|
|
40
|
+
document: [
|
|
41
|
+
'application/pdf',
|
|
42
|
+
'application/msword',
|
|
43
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
44
|
+
'application/vnd.ms-excel',
|
|
45
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
46
|
+
'text/plain',
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* File size limits (in bytes)
|
|
52
|
+
*/
|
|
53
|
+
export const FILE_SIZE_LIMITS = {
|
|
54
|
+
image: 10 * 1024 * 1024, // 10 MB
|
|
55
|
+
audio: 16 * 1024 * 1024, // 16 MB
|
|
56
|
+
video: 100 * 1024 * 1024, // 100 MB
|
|
57
|
+
document: 100 * 1024 * 1024, // 100 MB
|
|
58
|
+
};
|
package/src/models/message.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { MediaAttachment } from './mediaTypes.js';
|
|
2
|
+
|
|
3
|
+
export type MessageType = "text" | "media" | "system";
|
|
2
4
|
|
|
3
5
|
export interface Message {
|
|
4
6
|
id: string;
|
|
@@ -9,5 +11,6 @@ export interface Message {
|
|
|
9
11
|
iv: string;
|
|
10
12
|
timestamp: number;
|
|
11
13
|
type: MessageType;
|
|
14
|
+
media?: MediaAttachment; // Optional media attachment
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -1,7 +1,57 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Message } from "../models/message.js";
|
|
2
|
+
import { ConnectionState } from "../constants.js";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Transport adapter interface for network communication
|
|
6
|
+
*/
|
|
3
7
|
export interface TransportAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* Connect to the transport
|
|
10
|
+
* @param userId - User ID to connect as
|
|
11
|
+
*/
|
|
4
12
|
connect(userId: string): Promise<void>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Disconnect from the transport
|
|
16
|
+
*/
|
|
17
|
+
disconnect(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reconnect to the transport
|
|
21
|
+
*/
|
|
22
|
+
reconnect(): Promise<void>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send a message
|
|
26
|
+
* @param message - Message to send
|
|
27
|
+
*/
|
|
5
28
|
send(message: Message): Promise<void>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a message handler
|
|
32
|
+
* @param handler - Function to call when a message is received
|
|
33
|
+
*/
|
|
6
34
|
onMessage(handler: (message: Message) => void): void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register a connection state change handler
|
|
38
|
+
* @param handler - Function to call when connection state changes
|
|
39
|
+
*/
|
|
40
|
+
onConnectionStateChange?(handler: (state: ConnectionState) => void): void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register an error handler
|
|
44
|
+
* @param handler - Function to call when an error occurs
|
|
45
|
+
*/
|
|
46
|
+
onError?(handler: (error: Error) => void): void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the current connection state
|
|
50
|
+
*/
|
|
51
|
+
getConnectionState(): ConnectionState;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if transport is connected
|
|
55
|
+
*/
|
|
56
|
+
isConnected(): boolean;
|
|
7
57
|
}
|
|
@@ -1,24 +1,86 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
type MessageHandler = (message: Message) => void;
|
|
1
|
+
import { Message } from "../models/message.js";
|
|
2
|
+
import { TransportAdapter } from "./adapters.js";
|
|
3
|
+
import { ConnectionState } from "../constants.js";
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* In-memory transport for testing (no actual network communication)
|
|
7
|
+
*/
|
|
6
8
|
export class InMemoryTransport implements TransportAdapter {
|
|
7
|
-
private
|
|
8
|
-
private
|
|
9
|
+
private messageHandler: ((message: Message) => void) | null = null;
|
|
10
|
+
private connectionState: ConnectionState = ConnectionState.DISCONNECTED;
|
|
11
|
+
private stateHandler: ((state: ConnectionState) => void) | null = null;
|
|
12
|
+
private errorHandler: ((error: Error) => void) | null = null;
|
|
13
|
+
|
|
14
|
+
async connect(userId: string): Promise<void> {
|
|
15
|
+
this.connectionState = ConnectionState.CONNECTED;
|
|
16
|
+
if (this.stateHandler) {
|
|
17
|
+
this.stateHandler(this.connectionState);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async disconnect(): Promise<void> {
|
|
22
|
+
this.connectionState = ConnectionState.DISCONNECTED;
|
|
23
|
+
if (this.stateHandler) {
|
|
24
|
+
this.stateHandler(this.connectionState);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
|
|
10
|
-
async
|
|
11
|
-
this.
|
|
28
|
+
async reconnect(): Promise<void> {
|
|
29
|
+
this.connectionState = ConnectionState.CONNECTING;
|
|
30
|
+
if (this.stateHandler) {
|
|
31
|
+
this.stateHandler(this.connectionState);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Simulate reconnection delay
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
36
|
+
|
|
37
|
+
this.connectionState = ConnectionState.CONNECTED;
|
|
38
|
+
if (this.stateHandler) {
|
|
39
|
+
this.stateHandler(this.connectionState);
|
|
40
|
+
}
|
|
12
41
|
}
|
|
13
42
|
|
|
14
43
|
async send(message: Message): Promise<void> {
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
// In-memory transport just echoes back the message
|
|
45
|
+
if (this.messageHandler) {
|
|
46
|
+
// Simulate async delivery
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
this.messageHandler!(message);
|
|
49
|
+
}, 10);
|
|
17
50
|
}
|
|
18
|
-
this.handler?.(message);
|
|
19
51
|
}
|
|
20
52
|
|
|
21
|
-
onMessage(handler:
|
|
22
|
-
this.
|
|
53
|
+
onMessage(handler: (message: Message) => void): void {
|
|
54
|
+
this.messageHandler = handler;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onConnectionStateChange(handler: (state: ConnectionState) => void): void {
|
|
58
|
+
this.stateHandler = handler;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onError(handler: (error: Error) => void): void {
|
|
62
|
+
this.errorHandler = handler;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getConnectionState(): ConnectionState {
|
|
66
|
+
return this.connectionState;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isConnected(): boolean {
|
|
70
|
+
return this.connectionState === ConnectionState.CONNECTED;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Test helper to simulate receiving a message
|
|
74
|
+
simulateReceive(message: Message): void {
|
|
75
|
+
if (this.messageHandler) {
|
|
76
|
+
this.messageHandler(message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Test helper to simulate an error
|
|
81
|
+
simulateError(error: Error): void {
|
|
82
|
+
if (this.errorHandler) {
|
|
83
|
+
this.errorHandler(error);
|
|
84
|
+
}
|
|
23
85
|
}
|
|
24
86
|
}
|
|
@@ -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
|
+
}
|