@icgio/icg-exchanges-wrapper 1.14.256 → 1.14.258

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.
@@ -4,7 +4,111 @@ const DEFAULT_ORDERBOOK_LEVEL = 50
4
4
 
5
5
  let first_time = {},
6
6
  exchange_pair_rates = {},
7
- pair_exchange_rates = {}
7
+ pair_exchange_rates = {},
8
+ // Track active WebSocket connections per exchange to avoid duplicates
9
+ ws_connection_promises = {},
10
+ ws_connection_pairs = {},
11
+ // Track wrapped promises with fallback handlers to avoid duplicate handlers
12
+ ws_connection_promises_with_fallback = {},
13
+ // Track persistent WebSocket failures per pair to avoid repeated retries
14
+ ws_failed_pairs = {}, // { "ExchangeName": { "PAIR": { count: number, last_failed: timestamp } } }
15
+ WS_FAILURE_THRESHOLD = 3, // Skip WebSocket after 3 consecutive failures
16
+ WS_RETRY_INTERVAL_MS = 5 * 60 * 1000 // Retry WebSocket after 5 minutes
17
+
18
+ // ============ HELPER FUNCTIONS ============
19
+
20
+ /**
21
+ * Group exchanges by name and merge all pairs
22
+ * Returns: { exchange_instances: {}, exchange_all_pairs: {} }
23
+ */
24
+ function group_exchanges_by_name(Exchanges, exchange_pair_dict) {
25
+ let exchange_instances = {}
26
+ let exchange_all_pairs = {}
27
+ for (let Exchange of Exchanges) {
28
+ let Exchange_name = _.upperFirst(Exchange.name)
29
+ if (!exchange_pair_dict[Exchange_name]) {
30
+ continue
31
+ }
32
+ if (!exchange_instances[Exchange_name]) {
33
+ exchange_instances[Exchange_name] = Exchange
34
+ exchange_all_pairs[Exchange_name] = []
35
+ }
36
+ exchange_all_pairs[Exchange_name] = _.union(exchange_all_pairs[Exchange_name], exchange_pair_dict[Exchange_name])
37
+ }
38
+ return { exchange_instances, exchange_all_pairs }
39
+ }
40
+
41
+ /**
42
+ * Set data in both exchange_pair and pair_exchange structures
43
+ */
44
+ function set_pair_data(exchange_pair_dict, pair_exchange_dict, exchange_name, pair, data) {
45
+ _.set(exchange_pair_dict, [exchange_name, pair], data)
46
+ _.set(pair_exchange_dict, [pair, exchange_name], data)
47
+ }
48
+
49
+ /**
50
+ * Delete data from both exchange_pair and pair_exchange structures
51
+ */
52
+ function delete_pair_data(exchange_pair_dict, pair_exchange_dict, exchange_name, pair) {
53
+ if (exchange_pair_dict[exchange_name]) {
54
+ delete exchange_pair_dict[exchange_name][pair]
55
+ }
56
+ if (pair_exchange_dict[pair]) {
57
+ delete pair_exchange_dict[pair][exchange_name]
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Track WebSocket failure for a pair
63
+ */
64
+ function track_ws_failure(failed_pairs_dict, exchange_name, pair) {
65
+ if (!failed_pairs_dict[exchange_name]) {
66
+ failed_pairs_dict[exchange_name] = {}
67
+ }
68
+ if (!failed_pairs_dict[exchange_name][pair]) {
69
+ failed_pairs_dict[exchange_name][pair] = { count: 0, last_failed: 0 }
70
+ }
71
+ failed_pairs_dict[exchange_name][pair].count++
72
+ failed_pairs_dict[exchange_name][pair].last_failed = Date.now()
73
+ }
74
+
75
+ /**
76
+ * Reset WebSocket failure count for a pair (on success)
77
+ */
78
+ function reset_ws_failure(failed_pairs_dict, exchange_name, pair) {
79
+ if (failed_pairs_dict[exchange_name] && failed_pairs_dict[exchange_name][pair]) {
80
+ delete failed_pairs_dict[exchange_name][pair]
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Check if pair should skip WebSocket due to persistent failures
86
+ */
87
+ function should_skip_websocket(failed_pairs_dict, exchange_name, pair) {
88
+ const failure_info = failed_pairs_dict[exchange_name]?.[pair]
89
+ return (
90
+ failure_info &&
91
+ failure_info.count >= WS_FAILURE_THRESHOLD &&
92
+ (Date.now() - failure_info.last_failed) < WS_RETRY_INTERVAL_MS
93
+ )
94
+ }
95
+
96
+ /**
97
+ * Create WebSocket timeout checker
98
+ */
99
+ function create_ws_timeout_checker(Exchange_name, timeout_ms, max_checks, on_timeout) {
100
+ const start_time = Date.now()
101
+ let check_count = 0
102
+ return () => {
103
+ check_count++
104
+ const elapsed = Date.now() - start_time
105
+ if (elapsed > timeout_ms || check_count > max_checks) {
106
+ on_timeout(elapsed)
107
+ return true
108
+ }
109
+ return false
110
+ }
111
+ }
8
112
 
9
113
  const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enabled = true) {
10
114
  let checked_rates = {}
@@ -14,38 +118,76 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
14
118
  console.error('Exchanges should be an array')
15
119
  return
16
120
  } else {
17
- for (let Exchange of Exchanges) {
18
- let Exchange_name = _.upperFirst(Exchange.name)
19
- if (checked_rates[Exchange_name]) {
20
- continue
21
- }
22
- checked_rates[Exchange_name] = true
23
- if (!exchange_pair_dict[Exchange_name]) {
24
- continue
121
+ // First pass: group pairs by exchange name and find the Exchange instance
122
+ // This ensures ALL pairs for each exchange are collected BEFORE creating any WebSocket
123
+ const { exchange_instances, exchange_all_pairs } = group_exchanges_by_name(Exchanges, exchange_pair_dict)
124
+
125
+ // Second pass: process each unique exchange once
126
+ // Now we have all pairs collected, create ONE WebSocket per exchange with ALL pairs
127
+ for (let Exchange_name in exchange_instances) {
128
+ let Exchange = exchange_instances[Exchange_name]
129
+ let all_pairs = exchange_all_pairs[Exchange_name]
130
+
131
+ // Log how many pairs will be used for this exchange
132
+ if (all_pairs.length > 0 && Exchange.ws_market && ws_enabled) {
133
+ console.log(`[WebSocket Rates] ${Exchange_name}: Creating connection with ${all_pairs.length} pair(s): ${all_pairs.join(', ')}`)
25
134
  }
135
+
26
136
  if (Exchange.ws_market && ws_enabled) {
27
- let ws_promise = new Promise((resolve, reject) => {
28
- if (!Exchange.ws || !Exchange.ws.market) {
29
- console.error(`${Exchange_name} WebSocket not properly initialized`)
30
- reject(new Error(`WebSocket not initialized for ${Exchange_name}`))
31
- return
137
+ // Check if WebSocket is already opening/opened for this exchange
138
+ const ws_key = Exchange_name
139
+ const ws_status = Exchange.ws?.market?.status
140
+ const is_opening_or_opened = ws_status === 'opening' || ws_status === 'opened'
141
+
142
+ let ws_promise
143
+ let ws_promise_with_fallback
144
+ if (is_opening_or_opened && ws_connection_promises[ws_key]) {
145
+ // Reuse existing WebSocket connection promise
146
+ ws_promise = ws_connection_promises[ws_key]
147
+ // Merge pairs if needed
148
+ const existing_pairs = ws_connection_pairs[ws_key] || []
149
+ const new_pairs = _.difference(all_pairs, existing_pairs)
150
+ if (new_pairs.length > 0) {
151
+ ws_connection_pairs[ws_key] = _.union(existing_pairs, all_pairs)
152
+ // Add new pairs to existing connection if exchange supports it
153
+ // Note: This depends on exchange implementation - some may need to restart
154
+ // For now, we'll wait for existing connection and check if new pairs are already included
155
+ }
156
+ // Reuse wrapped promise with fallback if it exists (avoid duplicate catch handlers)
157
+ if (ws_connection_promises_with_fallback[ws_key]) {
158
+ ws_promise_with_fallback = ws_connection_promises_with_fallback[ws_key]
32
159
  }
160
+ } else {
161
+ // Create new WebSocket connection
162
+ ws_connection_pairs[ws_key] = all_pairs
163
+ ws_promise = new Promise((resolve, reject) => {
164
+ if (!Exchange.ws || !Exchange.ws.market) {
165
+ console.error(`${Exchange_name} WebSocket not properly initialized`)
166
+ reject(new Error(`WebSocket not initialized for ${Exchange_name}`))
167
+ return
168
+ }
33
169
 
34
- Exchange.ws_market(exchange_pair_dict[Exchange_name])
170
+ // Store promise before starting connection
171
+ ws_connection_promises[ws_key] = ws_promise
172
+
173
+ // Create WebSocket with ALL pairs collected in first pass
174
+ // This ensures only ONE WebSocket per exchange, even with multiple clients/pairs
175
+ Exchange.ws_market(all_pairs)
35
176
 
36
177
  const WS_TIMEOUT_MS = 30000 // 30 seconds timeout
37
- const start_time = Date.now()
38
- let check_count = 0
39
178
  const MAX_CHECKS = 300 // 30 seconds / 100ms
40
-
41
- const check_status = () => {
42
- check_count++
43
- const elapsed = Date.now() - start_time
44
-
45
- // Timeout check
46
- if (elapsed > WS_TIMEOUT_MS || check_count > MAX_CHECKS) {
179
+ const check_timeout = create_ws_timeout_checker(
180
+ Exchange_name,
181
+ WS_TIMEOUT_MS,
182
+ MAX_CHECKS,
183
+ (elapsed) => {
47
184
  console.error(`WebSocket timeout for ${Exchange_name} after ${elapsed}ms`)
48
185
  reject(new Error(`WebSocket timeout for ${Exchange_name}`))
186
+ }
187
+ )
188
+
189
+ const check_status = () => {
190
+ if (check_timeout()) {
49
191
  return
50
192
  }
51
193
 
@@ -56,74 +198,184 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
56
198
  return
57
199
  }
58
200
 
59
- let all_pairs_opened = exchange_pair_dict[Exchange_name].every((pair) => {
60
- return (
61
- Exchange.ws.market.pair_status &&
62
- Exchange.ws.market.pair_status[pair] === 'opened' &&
63
- Exchange.ws.market.asks_dict &&
64
- Exchange.ws.market.asks_dict[pair] &&
65
- Exchange.ws.market.asks_dict[pair].length > 0 &&
66
- Exchange.ws.market.bids_dict &&
67
- Exchange.ws.market.bids_dict[pair] &&
68
- Exchange.ws.market.bids_dict[pair].length > 0
69
- )
70
- })
71
- if (all_pairs_opened) {
72
- let rate_promises = exchange_pair_dict[Exchange_name].map((pair) => {
201
+ // Check only the pairs we need for this call (not all pairs in connection)
202
+ let pairs_to_check = exchange_pair_dict[Exchange_name] || all_pairs
203
+
204
+ // Check if WebSocket connection is at least opening/opened (not closed/stopped)
205
+ const ws_connection_ready = Exchange.ws.market.status === 'opening' || Exchange.ws.market.status === 'opened'
206
+
207
+ if (ws_connection_ready) {
208
+ // Try each pair individually - wait a bit for pairs to become ready
209
+ // This allows us to detect individual pair failures while giving time for subscription
210
+ const PAIR_READY_TIMEOUT_MS = 10000 // Wait up to 10 seconds for pair to become ready
211
+ const PAIR_CHECK_INTERVAL_MS = 100 // Check every 100ms
212
+
213
+ let rate_promises = pairs_to_check.map((pair) => {
73
214
  return new Promise((rate_resolve) => {
74
- Exchange.rate_ws(pair, function (res) {
75
- if (res.success) {
76
- _.set(exchange_pair_rates, [Exchange_name, pair], res.body)
77
- _.set(pair_exchange_rates, [pair, Exchange_name], res.body)
215
+ // Check if this pair should skip WebSocket due to persistent failures
216
+ if (should_skip_websocket(ws_failed_pairs, Exchange_name, pair)) {
217
+ const failure_info = ws_failed_pairs[Exchange_name][pair]
218
+ console.log(`[WebSocket Skip] ${Exchange_name} ${pair}: Skipping WebSocket (${failure_info.count} consecutive failures, last failed ${Math.round((Date.now() - failure_info.last_failed) / 1000)}s ago)`)
219
+ rate_resolve({ pair, failed: true, skip_ws: true })
220
+ return
221
+ }
222
+
223
+ // Wait for pair to become ready with retry logic
224
+ const pair_start_time = Date.now()
225
+ const check_pair_ready = () => {
226
+ // Check if this specific pair is ready before calling rate_ws
227
+ const pair_ready = (
228
+ Exchange.ws.market.pair_status &&
229
+ Exchange.ws.market.pair_status[pair] === 'opened' &&
230
+ Exchange.ws.market.asks_dict &&
231
+ Exchange.ws.market.asks_dict[pair] &&
232
+ Exchange.ws.market.asks_dict[pair].length > 0 &&
233
+ Exchange.ws.market.bids_dict &&
234
+ Exchange.ws.market.bids_dict[pair] &&
235
+ Exchange.ws.market.bids_dict[pair].length > 0
236
+ )
237
+
238
+ if (pair_ready) {
239
+ // Pair is ready, try to get rate
240
+ Exchange.rate_ws(pair, function (res) {
241
+ if (res.success) {
242
+ // Success - reset failure count
243
+ reset_ws_failure(ws_failed_pairs, Exchange_name, pair)
244
+ set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
245
+ rate_resolve({ pair, failed: false })
246
+ } else {
247
+ console.error(`getting rate ws failed for ${Exchange_name} ${pair}:`, res)
248
+ delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
249
+ track_ws_failure(ws_failed_pairs, Exchange_name, pair)
250
+ rate_resolve({ pair, failed: true })
251
+ }
252
+ })
78
253
  } else {
79
- console.error(`getting rate ws failed for ${Exchange_name} ${pair}:`, res)
80
- if (exchange_pair_rates[Exchange_name]) {
81
- delete exchange_pair_rates[Exchange_name][pair]
82
- }
83
- if (pair_exchange_rates[pair]) {
84
- delete pair_exchange_rates[pair][Exchange_name]
254
+ // Pair not ready yet - check if we've exceeded timeout
255
+ const elapsed = Date.now() - pair_start_time
256
+ if (elapsed >= PAIR_READY_TIMEOUT_MS) {
257
+ // Timeout reached - mark as failed, will fall back to REST
258
+ console.log(`[WebSocket] ${Exchange_name} ${pair}: Not ready after ${elapsed}ms (status: ${Exchange.ws.market.pair_status?.[pair] || 'unknown'})`)
259
+ rate_resolve({ pair, failed: true })
260
+ } else {
261
+ // Retry after a short delay
262
+ setTimeout(check_pair_ready, PAIR_CHECK_INTERVAL_MS)
85
263
  }
86
264
  }
87
- rate_resolve()
88
- })
265
+ }
266
+
267
+ // Start checking
268
+ check_pair_ready()
89
269
  })
90
270
  })
91
271
  Promise.all(rate_promises)
92
- .then(() => {
93
- // if (Exchange.ws.market.end_base) {
94
- // Exchange.ws.market.end_base(); // Close the connection and prevent reconnection
95
- // console.log(`${Exchange_name} WebSocket connection closed.`);
96
- // }
97
- resolve()
272
+ .then((results) => {
273
+ // Check which pairs failed and fall back to REST for those pairs only
274
+ const failed_pairs = results.filter((r) => r && r.failed).map((r) => r.pair)
275
+ if (failed_pairs.length > 0 && Exchange.rate) {
276
+ console.log(`[WebSocket Fallback] ${Exchange_name}: Falling back to REST for ${failed_pairs.length} pair(s): ${failed_pairs.join(', ')}`)
277
+ const rest_promises = failed_pairs.map((pair) => {
278
+ return new Promise((rest_resolve) => {
279
+ Exchange.rate(pair, DEFAULT_ORDERBOOK_LEVEL, function (res) {
280
+ if (res.success) {
281
+ set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
282
+ console.log(`[REST Fallback] ${Exchange_name} ${pair}: Successfully fetched via REST`)
283
+ } else {
284
+ console.error(`[REST Fallback] getting rate failed for ${Exchange_name} ${pair}:`, res)
285
+ delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
286
+ }
287
+ rest_resolve()
288
+ })
289
+ })
290
+ })
291
+ // Wait for REST fallback to complete before resolving
292
+ return Promise.all(rest_promises)
293
+ .catch((err) => {
294
+ console.error(`[REST Fallback] Error fetching rates for ${Exchange_name}:`, err)
295
+ })
296
+ .then(() => {
297
+ // Resolve after REST fallback completes
298
+ resolve()
299
+ })
300
+ } else {
301
+ // if (Exchange.ws.market.end_base) {
302
+ // Exchange.ws.market.end_base(); // Close the connection and prevent reconnection
303
+ // console.log(`${Exchange_name} WebSocket connection closed.`);
304
+ // }
305
+ resolve()
306
+ }
98
307
  })
99
308
  .catch((err) => {
100
309
  console.error(`Error in rate_ws for ${Exchange_name}:`, err)
101
310
  resolve()
102
311
  })
103
312
  } else {
313
+ // WebSocket connection not ready yet, wait and retry
104
314
  setTimeout(check_status, 100)
105
315
  }
106
316
  }
107
317
 
108
318
  check_status()
109
319
  })
320
+ // Clean up promise reference when done (but keep connection open for reuse)
321
+ ws_promise.finally(() => {
322
+ // Don't delete immediately - keep for a short time to allow reuse
323
+ setTimeout(() => {
324
+ if (ws_connection_promises[ws_key] === ws_promise) {
325
+ delete ws_connection_promises[ws_key]
326
+ // Also clean up wrapped promise if it exists
327
+ if (ws_connection_promises_with_fallback[ws_key]) {
328
+ delete ws_connection_promises_with_fallback[ws_key]
329
+ }
330
+ }
331
+ }, 1000)
332
+ })
333
+ }
110
334
 
111
- promises.push(ws_promise)
335
+ // Wrap WebSocket promise to handle rejection and fall back to REST
336
+ // Only create wrapper if it doesn't exist (avoid duplicate catch handlers on reused promises)
337
+ if (!ws_promise_with_fallback) {
338
+ ws_promise_with_fallback = ws_promise.catch((err) => {
339
+ console.error(`[WebSocket Failed] ${Exchange_name}: Falling back to REST for all pairs. Error:`, err.message)
340
+ // Fall back to REST for all pairs
341
+ if (Exchange.rate) {
342
+ return Promise.all(
343
+ all_pairs.map((pair) => {
344
+ return new Promise((rest_resolve) => {
345
+ Exchange.rate(pair, DEFAULT_ORDERBOOK_LEVEL, function (res) {
346
+ if (res.success) {
347
+ set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
348
+ console.log(`[REST Fallback] ${Exchange_name} ${pair}: Successfully fetched via REST`)
349
+ } else {
350
+ console.error(`[REST Fallback] getting rate failed for ${Exchange_name} ${pair}:`, res)
351
+ delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
352
+ }
353
+ rest_resolve()
354
+ })
355
+ })
356
+ })
357
+ ).then(() => {
358
+ // Resolve after REST fallback completes
359
+ return Promise.resolve()
360
+ })
361
+ }
362
+ // If no REST fallback available, still resolve to prevent Promise.all from failing
363
+ return Promise.resolve()
364
+ })
365
+ // Store wrapped promise for reuse
366
+ ws_connection_promises_with_fallback[ws_key] = ws_promise_with_fallback
367
+ }
368
+ promises.push(ws_promise_with_fallback)
112
369
  } else if (Exchange.rate) {
113
- let rate_promises = exchange_pair_dict[Exchange_name].map((pair) => {
370
+ let pairs_to_fetch = exchange_pair_dict[Exchange_name] || all_pairs
371
+ let rate_promises = pairs_to_fetch.map((pair) => {
114
372
  return new Promise((resolve, reject) => {
115
373
  Exchange.rate(pair, DEFAULT_ORDERBOOK_LEVEL, function (res) {
116
374
  if (res.success) {
117
- _.set(exchange_pair_rates, [Exchange_name, pair], res.body)
118
- _.set(pair_exchange_rates, [pair, Exchange_name], res.body)
375
+ set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, res.body)
119
376
  } else {
120
377
  console.error(`getting rate failed for ${Exchange_name} ${pair}:`, res)
121
- if (exchange_pair_rates[Exchange_name]) {
122
- delete exchange_pair_rates[Exchange_name][pair]
123
- }
124
- if (pair_exchange_rates[pair]) {
125
- delete pair_exchange_rates[pair][Exchange_name]
126
- }
378
+ delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
127
379
  }
128
380
  resolve()
129
381
  })
@@ -132,23 +384,18 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
132
384
 
133
385
  promises = promises.concat(rate_promises) // Add all rate promises
134
386
  } else if (Exchange.rate_with_routes) {
135
- let rate_with_routes_promises = exchange_pair_dict[Exchange_name].map((pair) => {
387
+ let pairs_to_fetch = exchange_pair_dict[Exchange_name] || all_pairs
388
+ let rate_with_routes_promises = pairs_to_fetch.map((pair) => {
136
389
  return new Promise((resolve, reject) => {
137
390
  Exchange.rate_with_routes(pair, function (res) {
138
391
  if (res.success) {
139
392
  let { asks, bids } = res.body
140
393
  asks = asks.map((ask) => ask.slice(0, 2))
141
394
  bids = bids.map((bid) => bid.slice(0, 2))
142
- _.set(exchange_pair_rates, [Exchange_name, pair], { asks, bids })
143
- _.set(pair_exchange_rates, [pair, Exchange_name], { asks, bids })
395
+ set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, { asks, bids })
144
396
  } else {
145
397
  console.error(`getting rate with routes failed for ${Exchange_name} ${pair}:`, res)
146
- if (exchange_pair_rates[Exchange_name]) {
147
- delete exchange_pair_rates[Exchange_name][pair]
148
- }
149
- if (pair_exchange_rates[pair]) {
150
- delete pair_exchange_rates[pair][Exchange_name]
151
- }
398
+ delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
152
399
  }
153
400
  resolve()
154
401
  })
@@ -172,7 +419,14 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
172
419
  }
173
420
 
174
421
  let exchange_pair_market_history = {},
175
- pair_exchange_market_history = {}
422
+ pair_exchange_market_history = {},
423
+ // Track active WebSocket connections for market history
424
+ ws_market_history_promises = {},
425
+ ws_market_history_pairs = {},
426
+ // Track wrapped promises with fallback handlers to avoid duplicate handlers
427
+ ws_market_history_promises_with_fallback = {},
428
+ // Track persistent WebSocket failures for market history
429
+ ws_market_history_failed_pairs = {} // { "ExchangeName": { "PAIR": { count: number, last_failed: timestamp } } }
176
430
  const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, timeframe_in_ms, cb, ws_enabled = true) {
177
431
  let checked_market_history = {}
178
432
  let promises = []
@@ -181,41 +435,71 @@ const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, ti
181
435
  console.error('Exchanges should be an array')
182
436
  return
183
437
  } else {
184
- for (let Exchange of Exchanges) {
185
- let Exchange_name = _.upperFirst(Exchange.name)
186
- if (checked_market_history[Exchange_name]) {
187
- continue
188
- }
189
- checked_market_history[Exchange_name] = true
190
- if (!exchange_pair_dict[Exchange_name]) {
191
- continue
192
- }
438
+ // First pass: group pairs by exchange name and find the Exchange instance
439
+ const { exchange_instances, exchange_all_pairs } = group_exchanges_by_name(Exchanges, exchange_pair_dict)
440
+
441
+ // Second pass: process each unique exchange once
442
+ for (let Exchange_name in exchange_instances) {
443
+ let Exchange = exchange_instances[Exchange_name]
444
+ let all_pairs = exchange_all_pairs[Exchange_name]
445
+
193
446
  if (Exchange.ws_market && ws_enabled) {
194
- let ws_promise = new Promise((resolve, reject) => {
195
- if (!Exchange.ws || !Exchange.ws.market) {
196
- console.error(`${Exchange_name} WebSocket not properly initialized`)
197
- reject(new Error(`WebSocket not initialized for ${Exchange_name}`))
198
- return
199
- }
447
+ // Check if WebSocket is already opening/opened for this exchange
448
+ const ws_key = `market_history_${Exchange_name}`
449
+ const ws_status = Exchange.ws?.market?.status
450
+ const is_opening_or_opened = ws_status === 'opening' || ws_status === 'opened'
200
451
 
201
- if (!first_time[Exchange_name]) {
202
- Exchange.ws_market(exchange_pair_dict[Exchange_name])
203
- first_time[Exchange_name] = true
452
+ let ws_promise
453
+ let ws_promise_with_fallback
454
+ if (is_opening_or_opened && ws_market_history_promises[ws_key]) {
455
+ // Reuse existing WebSocket connection promise
456
+ ws_promise = ws_market_history_promises[ws_key]
457
+ // Merge pairs if needed
458
+ const existing_pairs = ws_market_history_pairs[ws_key] || []
459
+ const new_pairs = _.difference(all_pairs, existing_pairs)
460
+ if (new_pairs.length > 0) {
461
+ ws_market_history_pairs[ws_key] = _.union(existing_pairs, all_pairs)
462
+ }
463
+ // Reuse wrapped promise with fallback if it exists (avoid duplicate catch handlers)
464
+ if (ws_market_history_promises_with_fallback[ws_key]) {
465
+ ws_promise_with_fallback = ws_market_history_promises_with_fallback[ws_key]
204
466
  }
467
+ } else {
468
+ // Create new WebSocket connection (or use existing one from rates)
469
+ ws_market_history_pairs[ws_key] = all_pairs
470
+ ws_promise = new Promise((resolve, reject) => {
471
+ if (!Exchange.ws || !Exchange.ws.market) {
472
+ console.error(`${Exchange_name} WebSocket not properly initialized`)
473
+ reject(new Error(`WebSocket not initialized for ${Exchange_name}`))
474
+ return
475
+ }
205
476
 
206
- const WS_TIMEOUT_MS = 30000 // 30 seconds timeout
207
- const start_time = Date.now()
208
- let check_count = 0
209
- const MAX_CHECKS = 300 // 30 seconds / 100ms
477
+ // Store promise before starting connection
478
+ ws_market_history_promises[ws_key] = ws_promise
210
479
 
211
- const check_status = () => {
212
- check_count++
213
- const elapsed = Date.now() - start_time
480
+ // Only start if not already started (reuse connection from rates if available)
481
+ // If starting new, use ALL pairs collected in first pass
482
+ if (!first_time[Exchange_name]) {
483
+ // Create WebSocket with ALL pairs collected in first pass
484
+ // This ensures only ONE WebSocket per exchange, even with multiple clients/pairs
485
+ Exchange.ws_market(all_pairs)
486
+ first_time[Exchange_name] = true
487
+ }
214
488
 
215
- // Timeout check
216
- if (elapsed > WS_TIMEOUT_MS || check_count > MAX_CHECKS) {
489
+ const WS_TIMEOUT_MS = 30000 // 30 seconds timeout
490
+ const MAX_CHECKS = 300 // 30 seconds / 100ms
491
+ const check_timeout = create_ws_timeout_checker(
492
+ Exchange_name,
493
+ WS_TIMEOUT_MS,
494
+ MAX_CHECKS,
495
+ (elapsed) => {
217
496
  console.error(`WebSocket timeout for ${Exchange_name} market history after ${elapsed}ms`)
218
497
  reject(new Error(`WebSocket timeout for ${Exchange_name} market history`))
498
+ }
499
+ )
500
+
501
+ const check_status = () => {
502
+ if (check_timeout()) {
219
503
  return
220
504
  }
221
505
 
@@ -226,37 +510,106 @@ const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, ti
226
510
  return
227
511
  }
228
512
 
229
- let all_pairs_opened = exchange_pair_dict[Exchange_name].every((pair) => {
230
- return (
231
- Exchange.ws.market.pair_status &&
232
- Exchange.ws.market.pair_status[pair] === 'opened' &&
233
- Exchange.ws.market.market_history_dict &&
234
- Exchange.ws.market.market_history_dict[pair] &&
235
- Exchange.ws.market.market_history_dict[pair].length >= 0
236
- )
237
- })
238
- if (all_pairs_opened) {
239
- let market_history_promises = exchange_pair_dict[Exchange_name].map((pair) => {
513
+ // Check only the pairs we need for this call
514
+ let pairs_to_check = exchange_pair_dict[Exchange_name] || all_pairs
515
+
516
+ // Check if WebSocket connection is at least opening/opened (not closed/stopped)
517
+ const ws_connection_ready = Exchange.ws.market.status === 'opening' || Exchange.ws.market.status === 'opened'
518
+
519
+ if (ws_connection_ready) {
520
+ // Try each pair individually - wait a bit for pairs to become ready
521
+ // This allows us to detect individual pair failures while giving time for subscription
522
+ const PAIR_READY_TIMEOUT_MS = 10000 // Wait up to 10 seconds for pair to become ready
523
+ const PAIR_CHECK_INTERVAL_MS = 100 // Check every 100ms
524
+
525
+ let market_history_promises = pairs_to_check.map((pair) => {
240
526
  return new Promise((rate_resolve) => {
241
- Exchange.market_history_ws(pair, timeframe_in_ms, function (res) {
242
- if (res.success) {
243
- _.set(exchange_pair_market_history, [Exchange_name, pair], res.body)
244
- _.set(pair_exchange_market_history, [pair, Exchange_name], res.body)
527
+ // Check if this pair should skip WebSocket due to persistent failures
528
+ if (should_skip_websocket(ws_market_history_failed_pairs, Exchange_name, pair)) {
529
+ const failure_info = ws_market_history_failed_pairs[Exchange_name][pair]
530
+ console.log(`[WebSocket Skip] ${Exchange_name} ${pair} market history: Skipping WebSocket (${failure_info.count} consecutive failures, last failed ${Math.round((Date.now() - failure_info.last_failed) / 1000)}s ago)`)
531
+ rate_resolve({ pair, failed: true, skip_ws: true })
532
+ return
533
+ }
534
+
535
+ // Wait for pair to become ready with retry logic
536
+ const pair_start_time = Date.now()
537
+ const check_pair_ready = () => {
538
+ // Check if this specific pair is ready before calling market_history_ws
539
+ const pair_ready = (
540
+ Exchange.ws.market.pair_status &&
541
+ Exchange.ws.market.pair_status[pair] === 'opened' &&
542
+ Exchange.ws.market.market_history_dict &&
543
+ Exchange.ws.market.market_history_dict[pair] &&
544
+ Exchange.ws.market.market_history_dict[pair].length >= 0
545
+ )
546
+
547
+ if (pair_ready) {
548
+ // Pair is ready, try to get market history
549
+ Exchange.market_history_ws(pair, timeframe_in_ms, function (res) {
550
+ if (res.success) {
551
+ // Success - reset failure count
552
+ reset_ws_failure(ws_market_history_failed_pairs, Exchange_name, pair)
553
+ set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
554
+ rate_resolve({ pair, failed: false })
555
+ } else {
556
+ console.error(`getting market history ws failed for ${Exchange_name} ${pair}:`, res)
557
+ delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
558
+ track_ws_failure(ws_market_history_failed_pairs, Exchange_name, pair)
559
+ rate_resolve({ pair, failed: true })
560
+ }
561
+ })
245
562
  } else {
246
- console.error(`getting market history ws failed for ${Exchange_name} ${pair}:`, res)
247
- if (exchange_pair_market_history[Exchange_name]) {
248
- delete exchange_pair_market_history[Exchange_name][pair]
249
- }
250
- if (pair_exchange_market_history[pair]) {
251
- delete pair_exchange_market_history[pair][Exchange_name]
563
+ // Pair not ready yet - check if we've exceeded timeout
564
+ const elapsed = Date.now() - pair_start_time
565
+ if (elapsed >= PAIR_READY_TIMEOUT_MS) {
566
+ // Timeout reached - mark as failed, will fall back to REST
567
+ console.log(`[WebSocket Market History] ${Exchange_name} ${pair}: Not ready after ${elapsed}ms (status: ${Exchange.ws.market.pair_status?.[pair] || 'unknown'})`)
568
+ rate_resolve({ pair, failed: true })
569
+ } else {
570
+ // Retry after a short delay
571
+ setTimeout(check_pair_ready, PAIR_CHECK_INTERVAL_MS)
252
572
  }
253
573
  }
254
- rate_resolve()
255
- })
574
+ }
575
+
576
+ // Start checking
577
+ check_pair_ready()
256
578
  })
257
579
  })
258
580
  Promise.all(market_history_promises)
259
- .then(() => resolve())
581
+ .then((results) => {
582
+ // Check which pairs failed and fall back to REST for those pairs only
583
+ const failed_pairs = results.filter((r) => r && r.failed).map((r) => r.pair)
584
+ if (failed_pairs.length > 0 && Exchange.market_history) {
585
+ console.log(`[WebSocket Fallback] ${Exchange_name} market history: Falling back to REST for ${failed_pairs.length} pair(s): ${failed_pairs.join(', ')}`)
586
+ const rest_promises = failed_pairs.map((pair) => {
587
+ return new Promise((rest_resolve) => {
588
+ Exchange.market_history(pair, timeframe_in_ms, function (res) {
589
+ if (res.success) {
590
+ set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
591
+ console.log(`[REST Fallback] ${Exchange_name} ${pair} market history: Successfully fetched via REST`)
592
+ } else {
593
+ console.error(`[REST Fallback] getting market history failed for ${Exchange_name} ${pair}:`, res)
594
+ delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
595
+ }
596
+ rest_resolve()
597
+ })
598
+ })
599
+ })
600
+ // Wait for REST fallback to complete before resolving
601
+ return Promise.all(rest_promises)
602
+ .catch((err) => {
603
+ console.error(`[REST Fallback] Error fetching market history for ${Exchange_name}:`, err)
604
+ })
605
+ .then(() => {
606
+ // Resolve after REST fallback completes
607
+ resolve()
608
+ })
609
+ } else {
610
+ resolve()
611
+ }
612
+ })
260
613
  .catch((err) => {
261
614
  console.error(`Error in market_history_ws for ${Exchange_name}:`, err)
262
615
  resolve()
@@ -267,22 +620,64 @@ const get_exchanges_market_history = function (Exchanges, exchange_pair_dict, ti
267
620
  }
268
621
  check_status()
269
622
  })
270
- promises.push(ws_promise)
623
+ // Clean up promise reference when done (but keep connection open for reuse)
624
+ ws_promise.finally(() => {
625
+ setTimeout(() => {
626
+ if (ws_market_history_promises[ws_key] === ws_promise) {
627
+ delete ws_market_history_promises[ws_key]
628
+ // Also clean up wrapped promise if it exists
629
+ if (ws_market_history_promises_with_fallback[ws_key]) {
630
+ delete ws_market_history_promises_with_fallback[ws_key]
631
+ }
632
+ }
633
+ }, 1000)
634
+ })
635
+ }
636
+
637
+ // If WebSocket promise rejects (timeout/error), fall back to REST for all pairs
638
+ // Only create wrapper if it doesn't exist (avoid duplicate catch handlers on reused promises)
639
+ if (!ws_promise_with_fallback) {
640
+ ws_promise_with_fallback = ws_promise.catch((err) => {
641
+ console.error(`[WebSocket Failed] ${Exchange_name} market history: Falling back to REST for all pairs. Error:`, err.message)
642
+ // Fall back to REST for all pairs
643
+ if (Exchange.market_history) {
644
+ return Promise.all(
645
+ all_pairs.map((pair) => {
646
+ return new Promise((rest_resolve) => {
647
+ Exchange.market_history(pair, timeframe_in_ms, function (res) {
648
+ if (res.success) {
649
+ set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
650
+ console.log(`[REST Fallback] ${Exchange_name} ${pair} market history: Successfully fetched via REST`)
651
+ } else {
652
+ console.error(`[REST Fallback] getting market history failed for ${Exchange_name} ${pair}:`, res)
653
+ delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
654
+ }
655
+ rest_resolve()
656
+ })
657
+ })
658
+ })
659
+ ).then(() => {
660
+ // Resolve after REST fallback completes
661
+ return Promise.resolve()
662
+ })
663
+ }
664
+ // If no REST fallback available, still resolve to prevent Promise.all from failing
665
+ return Promise.resolve()
666
+ })
667
+ // Store wrapped promise for reuse
668
+ ws_market_history_promises_with_fallback[ws_key] = ws_promise_with_fallback
669
+ }
670
+ promises.push(ws_promise_with_fallback)
271
671
  } else if (Exchange.market_history) {
272
- let market_history_promises = exchange_pair_dict[Exchange_name].map((pair) => {
672
+ let pairs_to_fetch = exchange_pair_dict[Exchange_name] || all_pairs
673
+ let market_history_promises = pairs_to_fetch.map((pair) => {
273
674
  return new Promise((resolve) => {
274
675
  Exchange.market_history(pair, timeframe_in_ms, function (res) {
275
676
  if (res.success) {
276
- _.set(exchange_pair_market_history, [Exchange_name, pair], res.body)
277
- _.set(pair_exchange_market_history, [pair, Exchange_name], res.body)
677
+ set_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair, res.body)
278
678
  } else {
279
679
  console.error(`getting market history failed for ${Exchange_name} ${pair}:`, res)
280
- if (exchange_pair_market_history[Exchange_name]) {
281
- delete exchange_pair_market_history[Exchange_name][pair]
282
- }
283
- if (pair_exchange_market_history[pair]) {
284
- delete pair_exchange_market_history[pair][Exchange_name]
285
- }
680
+ delete_pair_data(exchange_pair_market_history, pair_exchange_market_history, Exchange_name, pair)
286
681
  }
287
682
  resolve()
288
683
  })
@@ -330,16 +725,10 @@ const get_exchanges_ohlcv = function (Exchanges, exchange_pair_dict, from_in_ms,
330
725
  if (Exchange.ohlcv) {
331
726
  Exchange.ohlcv(pair, from_in_ms, to_in_ms, function (res) {
332
727
  if (res.success) {
333
- _.set(exchange_pair_ohlcv, [Exchange_name, pair], res.body)
334
- _.set(pair_exchange_ohlcv, [pair, Exchange_name], res.body)
728
+ set_pair_data(exchange_pair_ohlcv, pair_exchange_ohlcv, Exchange_name, pair, res.body)
335
729
  } else {
336
730
  console.error(`getting ohlcv failed for ${Exchange_name} ${pair}:`, res)
337
- if (exchange_pair_ohlcv[Exchange_name]) {
338
- delete exchange_pair_ohlcv[Exchange_name][pair]
339
- }
340
- if (pair_exchange_ohlcv[pair]) {
341
- delete pair_exchange_ohlcv[pair][Exchange_name]
342
- }
731
+ delete_pair_data(exchange_pair_ohlcv, pair_exchange_ohlcv, Exchange_name, pair)
343
732
  }
344
733
  _.set(ohlcv_status, [Exchange_name + pair], true)
345
734
  // Use a flag to prevent multiple callback calls
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icgio/icg-exchanges-wrapper",
3
- "version": "1.14.256",
3
+ "version": "1.14.258",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {