@nexustechpro/baileys 1.1.4 → 1.1.7

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.
@@ -2,57 +2,310 @@ import WebSocket from 'ws'
2
2
  import { DEFAULT_ORIGIN } from '../../Defaults/index.js'
3
3
  import { AbstractSocketClient } from './types.js'
4
4
 
5
+ // ==================== CONSTANTS ====================
6
+ const CONSTANTS = {
7
+ MIN_SEND_INTERVAL_MS: 50,
8
+ MAX_RECONNECT_ATTEMPTS: 5,
9
+ INITIAL_RECONNECT_DELAY: 1000,
10
+ MAX_RECONNECT_DELAY: 30000,
11
+ RECONNECT_BACKOFF_MULTIPLIER: 2
12
+ }
13
+
14
+ // ==================== WEBSOCKET CLIENT ====================
5
15
  export class WebSocketClient extends AbstractSocketClient {
6
- socket = null
16
+ constructor() {
17
+ super(...arguments)
18
+
19
+ // Core socket
20
+ this.socket = null
21
+
22
+ // Message queue
23
+ this._queue = []
24
+ this._isDispatching = false
25
+ this._lastDispatch = 0
26
+ this._minSendIntervalMs = CONSTANTS.MIN_SEND_INTERVAL_MS
27
+
28
+ // Reconnection state
29
+ this._reconnectTimeout = null
30
+ this._reconnectAttempts = 0
31
+ this._maxReconnectAttempts = CONSTANTS.MAX_RECONNECT_ATTEMPTS
32
+ this._reconnectDelay = CONSTANTS.INITIAL_RECONNECT_DELAY
33
+ this._shouldReconnect = true
34
+ this._isManualClose = false
35
+ this._isReconnecting = false
36
+ }
7
37
 
38
+ // ==================== LOGGER ====================
39
+ get logger() {
40
+ return this.config?.logger || console
41
+ }
42
+
43
+ // ==================== CONNECTION STATE ====================
8
44
  get isOpen() {
9
45
  return this.socket?.readyState === WebSocket.OPEN
10
46
  }
11
47
 
12
48
  get isClosed() {
13
- return this.socket === null || this.socket?.readyState === WebSocket.CLOSED
49
+ return !this.socket || this.socket.readyState === WebSocket.CLOSED
14
50
  }
15
51
 
16
52
  get isClosing() {
17
- return this.socket === null || this.socket?.readyState === WebSocket.CLOSING
53
+ return !this.socket || this.socket.readyState === WebSocket.CLOSING
18
54
  }
19
55
 
20
56
  get isConnecting() {
21
57
  return this.socket?.readyState === WebSocket.CONNECTING
22
58
  }
23
59
 
24
- connect() {
25
- if (this.socket) return
60
+ // ==================== CONNECTION MANAGEMENT ====================
61
+ async connect() {
62
+ if (this.socket && !this.isClosed) {
63
+ this.logger.debug({ state: this.socket.readyState }, 'already connected or connecting')
64
+ return
65
+ }
66
+
67
+ try {
68
+ this.logger.debug('establishing websocket connection')
69
+
70
+ this.socket = new WebSocket(this.url, {
71
+ origin: DEFAULT_ORIGIN,
72
+ headers: this.config.options?.headers,
73
+ handshakeTimeout: this.config.connectTimeoutMs,
74
+ timeout: this.config.connectTimeoutMs,
75
+ agent: this.config.agent
76
+ })
26
77
 
27
- this.socket = new WebSocket(this.url, {
28
- origin: DEFAULT_ORIGIN,
29
- headers: this.config.options?.headers || {},
30
- handshakeTimeout: this.config.connectTimeoutMs,
31
- timeout: this.config.connectTimeoutMs,
32
- agent: this.config.agent
33
- })
78
+ if (!this.socket) {
79
+ throw new Error('WebSocket creation failed')
80
+ }
34
81
 
35
- this.socket.setMaxListeners(0)
82
+ this.socket.setMaxListeners(0)
36
83
 
37
- const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
38
- for (const event of events) this.socket?.on(event, (...args) => this.emit(event, ...args))
84
+ // Forward all WebSocket events
85
+ const events = ['error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
86
+ events.forEach(e => {
87
+ this.socket?.on(e, (...args) => this.emit(e, ...args))
88
+ })
89
+
90
+ // Handle close with auto-reconnect
91
+ this.socket.on('close', (...args) => {
92
+ this.emit('close', ...args)
93
+ if (this._shouldReconnect && !this._isManualClose) {
94
+ this._attemptReconnect()
95
+ }
96
+ })
97
+
98
+ // Handle successful connection
99
+ this.socket.on('open', () => {
100
+ this.logger.info('websocket connection established')
101
+ this._reconnectAttempts = 0
102
+ this._reconnectDelay = CONSTANTS.INITIAL_RECONNECT_DELAY
103
+ this._isReconnecting = false
104
+
105
+ // Process any queued messages
106
+ if (this._queue.length > 0) {
107
+ this.logger.debug({ queueLength: this._queue.length }, 'processing queued messages')
108
+ this._dispatch()
109
+ }
110
+ })
111
+
112
+ } catch (error) {
113
+ this.logger.error({ error: error.message }, 'websocket connection failed')
114
+ this.socket = null
115
+ throw error
116
+ }
39
117
  }
40
118
 
41
119
  async close() {
42
- if (!this.socket) return
43
- const closePromise = new Promise(resolve => this.socket?.once('close', resolve))
44
- this.socket.close()
45
- await closePromise
120
+ this.logger.debug('closing websocket connection (manual)')
121
+
122
+ this._isManualClose = true
123
+ this._shouldReconnect = false
124
+ this._isReconnecting = false
125
+
126
+ if (this._reconnectTimeout) {
127
+ clearTimeout(this._reconnectTimeout)
128
+ this._reconnectTimeout = null
129
+ }
130
+
131
+ this.socket?.close?.()
46
132
  this.socket = null
133
+ this._queue = []
47
134
  }
48
135
 
136
+ async restart() {
137
+ this.logger.info('restarting websocket connection')
138
+
139
+ this._isManualClose = true
140
+ this._isReconnecting = false
141
+
142
+ // Force close existing connection
143
+ if (this.socket) {
144
+ await new Promise(resolve => {
145
+ this.socket.once('close', resolve)
146
+ this.socket.terminate()
147
+ })
148
+ this.socket = null
149
+ }
150
+
151
+ // Clear queue and reset state
152
+ this._queue = []
153
+ this._reconnectDelay = CONSTANTS.INITIAL_RECONNECT_DELAY
154
+ this._isManualClose = false
155
+ this._shouldReconnect = true
156
+
157
+ // Reconnect
158
+ await this.connect()
159
+ }
160
+
161
+ // ==================== RECONNECTION LOGIC ====================
162
+ _attemptReconnect() {
163
+ if (this._isReconnecting) {
164
+ this.logger.trace('reconnection already in progress')
165
+ return
166
+ }
167
+
168
+ if (this._reconnectAttempts >= this._maxReconnectAttempts) {
169
+ this.logger.error(
170
+ { attempts: this._reconnectAttempts, max: this._maxReconnectAttempts },
171
+ 'max reconnect attempts reached'
172
+ )
173
+ this.emit('reconnect-failed')
174
+ return
175
+ }
176
+
177
+ if (this._reconnectTimeout) {
178
+ clearTimeout(this._reconnectTimeout)
179
+ }
180
+
181
+ this._isReconnecting = true
182
+ this._reconnectAttempts++
183
+
184
+ this.logger.info(
185
+ {
186
+ attempt: this._reconnectAttempts,
187
+ maxAttempts: this._maxReconnectAttempts,
188
+ delay: this._reconnectDelay
189
+ },
190
+ 'attempting websocket reconnection'
191
+ )
192
+
193
+ this._reconnectTimeout = setTimeout(async () => {
194
+ try {
195
+ await this.connect()
196
+ } catch (error) {
197
+ this.logger.warn({ error: error.message }, 'reconnection attempt failed')
198
+ this._isReconnecting = false
199
+
200
+ if (this._reconnectAttempts < this._maxReconnectAttempts) {
201
+ this._attemptReconnect()
202
+ } else {
203
+ this.emit('reconnect-failed')
204
+ }
205
+ }
206
+ }, this._reconnectDelay)
207
+
208
+ // Exponential backoff with cap
209
+ this._reconnectDelay = Math.min(
210
+ this._reconnectDelay * CONSTANTS.RECONNECT_BACKOFF_MULTIPLIER,
211
+ CONSTANTS.MAX_RECONNECT_DELAY
212
+ )
213
+ }
214
+
215
+ // ==================== MESSAGE SENDING ====================
49
216
  send(str, cb) {
50
- this.socket?.send(str, cb)
51
- return Boolean(this.socket)
217
+ const doSend = () => {
218
+ // Handle closed/closing socket
219
+ if (this.isClosed || this.isClosing) {
220
+ this.logger.warn('socket closed, attempting reconnection')
221
+
222
+ this._isManualClose = false
223
+ this._shouldReconnect = true
224
+ this._attemptReconnect()
225
+ this._queue.unshift(doSend) // Re-queue at front
226
+
227
+ cb?.(new Error('Socket closed, reconnecting...'))
228
+ return false
229
+ }
230
+
231
+ // Check if socket is ready
232
+ if (!this.socket || !this.isOpen) {
233
+ cb?.(new Error('Socket not open'))
234
+ return false
235
+ }
236
+
237
+ // Send the message
238
+ try {
239
+ this.socket.send(str, cb)
240
+ return true
241
+ } catch (error) {
242
+ this.logger.error({ error: error.message }, 'failed to send message')
243
+ cb?.(error)
244
+ return false
245
+ }
246
+ }
247
+
248
+ this._queue.push(doSend)
249
+ this._dispatch()
250
+ return true
52
251
  }
53
252
 
54
- async restart() {
55
- await this.close()
56
- this.connect()
253
+ // ==================== MESSAGE QUEUE DISPATCH ====================
254
+ _dispatch() {
255
+ // Don't dispatch if already dispatching or socket not ready
256
+ if (this._isDispatching || (!this.isOpen && !this.isConnecting)) {
257
+ return
258
+ }
259
+
260
+ const now = Date.now()
261
+ const elapsed = now - this._lastDispatch
262
+
263
+ // Check if enough time has passed and queue has items
264
+ if (this._queue.length && elapsed >= this._minSendIntervalMs) {
265
+ this._isDispatching = true
266
+
267
+ const sendFn = this._queue.shift()
268
+ sendFn?.()
269
+
270
+ this._lastDispatch = Date.now()
271
+ this._isDispatching = false
272
+
273
+ // Schedule next dispatch if queue not empty
274
+ if (this._queue.length) {
275
+ const nextDelay = Math.max(0, this._minSendIntervalMs - (Date.now() - this._lastDispatch))
276
+ setTimeout(() => this._dispatch(), nextDelay)
277
+ }
278
+ } else if (this._queue.length) {
279
+ // Schedule dispatch after required interval
280
+ const delay = Math.max(0, this._minSendIntervalMs - elapsed)
281
+ setTimeout(() => this._dispatch(), delay)
282
+ }
57
283
  }
58
- }
284
+
285
+ // ==================== PUBLIC CONTROL METHODS ====================
286
+ disableAutoReconnect() {
287
+ this.logger.info('auto-reconnect disabled')
288
+ this._shouldReconnect = false
289
+ }
290
+
291
+ enableAutoReconnect() {
292
+ this.logger.info('auto-reconnect enabled')
293
+ this._shouldReconnect = true
294
+ this._isManualClose = false
295
+ }
296
+
297
+ // Getters for external monitoring
298
+ get reconnectAttempts() {
299
+ return this._reconnectAttempts
300
+ }
301
+
302
+ get queueLength() {
303
+ return this._queue.length
304
+ }
305
+
306
+ get isReconnecting() {
307
+ return this._isReconnecting
308
+ }
309
+ }
310
+
311
+ export default WebSocketClient