tardis-machine 18.4.2 → 18.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
  2. package/dist/ws/subscriptionsmappers.js +11 -1
  3. package/dist/ws/subscriptionsmappers.js.map +1 -1
  4. package/node_modules/tardis-dev/dist/consts.d.ts +1 -1
  5. package/node_modules/tardis-dev/dist/consts.js +1 -1
  6. package/node_modules/tardis-dev/dist/consts.js.map +1 -1
  7. package/node_modules/tardis-dev/dist/downloaddatasets.d.ts.map +1 -1
  8. package/node_modules/tardis-dev/dist/downloaddatasets.js +10 -4
  9. package/node_modules/tardis-dev/dist/downloaddatasets.js.map +1 -1
  10. package/node_modules/tardis-dev/dist/handy.d.ts +2 -1
  11. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  12. package/node_modules/tardis-dev/dist/handy.js +16 -7
  13. package/node_modules/tardis-dev/dist/handy.js.map +1 -1
  14. package/node_modules/tardis-dev/dist/mappers/bitflyer.js +3 -3
  15. package/node_modules/tardis-dev/dist/mappers/bitflyer.js.map +1 -1
  16. package/node_modules/tardis-dev/dist/mappers/bitget.js +9 -9
  17. package/node_modules/tardis-dev/dist/mappers/bitget.js.map +1 -1
  18. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +3 -2
  19. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts.map +1 -1
  20. package/node_modules/tardis-dev/dist/mappers/gateiofutures.js +61 -6
  21. package/node_modules/tardis-dev/dist/mappers/gateiofutures.js.map +1 -1
  22. package/node_modules/tardis-dev/dist/mappers/index.d.ts +2 -2
  23. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.d.ts.map +1 -1
  24. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js +3 -11
  25. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js.map +1 -1
  26. package/node_modules/tardis-dev/dist/realtimefeeds/gateiofutures.d.ts +4 -1
  27. package/node_modules/tardis-dev/dist/realtimefeeds/gateiofutures.d.ts.map +1 -1
  28. package/node_modules/tardis-dev/dist/realtimefeeds/gateiofutures.js +42 -7
  29. package/node_modules/tardis-dev/dist/realtimefeeds/gateiofutures.js.map +1 -1
  30. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts +6 -2
  31. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  32. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +73 -26
  33. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  34. package/node_modules/tardis-dev/package.json +1 -1
  35. package/node_modules/tardis-dev/src/consts.ts +1 -1
  36. package/node_modules/tardis-dev/src/downloaddatasets.ts +13 -4
  37. package/node_modules/tardis-dev/src/handy.ts +17 -7
  38. package/node_modules/tardis-dev/src/mappers/bitflyer.ts +3 -3
  39. package/node_modules/tardis-dev/src/mappers/bitget.ts +9 -9
  40. package/node_modules/tardis-dev/src/mappers/gateiofutures.ts +89 -7
  41. package/node_modules/tardis-dev/src/realtimefeeds/coinbasederivatives.ts +3 -11
  42. package/node_modules/tardis-dev/src/realtimefeeds/gateiofutures.ts +80 -8
  43. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +89 -28
  44. package/package.json +2 -2
  45. package/src/ws/subscriptionsmappers.ts +12 -1
@@ -1,5 +1,5 @@
1
1
  import { Writable } from 'node:stream'
2
- import { batch, getJSON, optimizeFilters } from '../handy.ts'
2
+ import { getJSON, optimizeFilters } from '../handy.ts'
3
3
  import { Filter } from '../types.ts'
4
4
  import { MultiConnectionRealTimeFeedBase, PoolingClientBase, RealTimeFeedBase } from './realtimefeed.ts'
5
5
 
@@ -12,16 +12,8 @@ export class CoinbaseDerivativesRealTimeFeed extends MultiConnectionRealTimeFeed
12
12
  }
13
13
  }
14
14
 
15
- const symbols = [...new Set(wsFilters.flatMap((f) => f.symbols ?? []))]
16
- const groups: Iterable<string[]> = symbols.length > 0 ? batch(symbols, 25) : wsFilters.length > 0 ? [[]] : []
17
- for (const group of groups) {
18
- const groupedFilters = wsFilters
19
- .map((f) => ({
20
- ...f,
21
- symbols: f.symbols?.filter((s) => group.includes(s))
22
- }))
23
- .filter((f) => f.symbols?.length || ['heartbeats', 'subscriptions'].includes(f.channel))
24
- yield new CoinbaseDerivativesSingleConnectionRealTimeFeed(exchange, groupedFilters, timeoutIntervalMS, onError)
15
+ if (wsFilters.length > 0) {
16
+ yield new CoinbaseDerivativesSingleConnectionRealTimeFeed(exchange, wsFilters, timeoutIntervalMS, onError)
25
17
  }
26
18
 
27
19
  const productFilters = filters.filter((f) => f.channel === 'products')
@@ -9,16 +9,66 @@ export class GateIOFuturesRealTimeFeed extends MultiConnectionRealTimeFeedBase {
9
9
  )
10
10
 
11
11
  const inverseContractsFilters = filters.reduce(
12
- this._only((s) => s.endsWith('_USDT') === false),
12
+ this._only((s) => s.endsWith('_USD')),
13
13
  [] as Filter<string>[]
14
14
  )
15
15
 
16
16
  if (linearContractsFilters.length > 0) {
17
- yield new GateIOFuturesSingleConnectionRealTimeFeed('usdt', exchange, linearContractsFilters, timeoutIntervalMS, onError)
17
+ yield new GateIOFuturesSingleConnectionRealTimeFeed(
18
+ 'wss://fx-ws.gateio.ws/v4/ws/usdt',
19
+ 400,
20
+ exchange,
21
+ linearContractsFilters,
22
+ timeoutIntervalMS,
23
+ onError
24
+ )
18
25
  }
19
26
 
20
27
  if (inverseContractsFilters.length > 0) {
21
- yield new GateIOFuturesSingleConnectionRealTimeFeed('btc', exchange, inverseContractsFilters, timeoutIntervalMS, onError)
28
+ yield new GateIOFuturesSingleConnectionRealTimeFeed(
29
+ 'wss://fx-ws.gateio.ws/v4/ws/btc',
30
+ 400,
31
+ exchange,
32
+ inverseContractsFilters,
33
+ timeoutIntervalMS,
34
+ onError
35
+ )
36
+ }
37
+
38
+ const usd1ContractsFilters = filters.reduce(
39
+ this._only((s) => s.endsWith('_USD1')),
40
+ [] as Filter<string>[]
41
+ )
42
+ if (usd1ContractsFilters.length > 0) {
43
+ yield new GateIOFuturesSingleConnectionRealTimeFeed(
44
+ 'wss://fx-ws.gateio.ws/v4/ws/usd1',
45
+ 400,
46
+ exchange,
47
+ usd1ContractsFilters,
48
+ timeoutIntervalMS,
49
+ onError
50
+ )
51
+ }
52
+
53
+ // Gate has no public liquidations channel for delivery futures
54
+ if (filters.every((filter) => filter.channel === 'public_liquidates' && filter.symbols!.every((s) => /_USDT_\d{8}$/.test(s)))) {
55
+ throw new Error('Gate.io delivery futures do not support public_liquidates')
56
+ }
57
+ const deliveryContractsFilters = filters
58
+ .filter((filter) => filter.channel !== 'public_liquidates')
59
+ .reduce(
60
+ this._only((s) => /_USDT_\d{8}$/.test(s)),
61
+ [] as Filter<string>[]
62
+ )
63
+ if (deliveryContractsFilters.length > 0) {
64
+ yield new GateIOFuturesSingleConnectionRealTimeFeed(
65
+ 'wss://fx-ws.gateio.ws/v4/ws/delivery/usdt',
66
+ 100,
67
+ exchange,
68
+ deliveryContractsFilters,
69
+ timeoutIntervalMS,
70
+ onError
71
+ )
22
72
  }
23
73
  }
24
74
 
@@ -27,6 +77,11 @@ export class GateIOFuturesRealTimeFeed extends MultiConnectionRealTimeFeedBase {
27
77
  if (!current.symbols || current.symbols.length === 0) {
28
78
  throw new Error('GateIOFuturesRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
29
79
  }
80
+ for (const symbol of current.symbols) {
81
+ if (!/_(USD|USDT|USD1)$/.test(symbol) && !/_USDT_\d{8}$/.test(symbol)) {
82
+ throw new Error(`Unsupported Gate.io futures symbol: ${symbol}`)
83
+ }
84
+ }
30
85
 
31
86
  const symbols = current.symbols.filter(filter)
32
87
  if (symbols.length > 0) {
@@ -41,28 +96,29 @@ export class GateIOFuturesRealTimeFeed extends MultiConnectionRealTimeFeedBase {
41
96
  }
42
97
 
43
98
  class GateIOFuturesSingleConnectionRealTimeFeed extends RealTimeFeedBase {
44
- protected readonly wssURL: string
45
99
  protected readonly extraHeaders = { 'X-Gate-Size-Decimal': '1' }
100
+ private readonly bookSequences = new Map<string, number>()
46
101
 
47
102
  constructor(
48
- wsURLSuffix: string,
103
+ protected readonly wssURL: string,
104
+ private readonly orderBookDepth: 100 | 400,
49
105
  exchange: string,
50
106
  filters: Filter<string>[],
51
107
  timeoutIntervalMS?: number,
52
108
  onError?: (error: Error) => void
53
109
  ) {
54
110
  super(exchange, filters, timeoutIntervalMS, onError)
55
- this.wssURL = `wss://fx-ws.gateio.ws/v4/ws/${wsURLSuffix}`
56
111
  }
57
112
 
58
113
  protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
114
+ this.bookSequences.clear()
59
115
  const payload = filters.flatMap((filter) => {
60
- if (filter.channel === 'order_book') {
116
+ if (filter.channel === 'order_book' || filter.channel === 'obu') {
61
117
  return filter.symbols!.map((symbol) => {
62
118
  return {
63
119
  event: 'subscribe',
64
120
  channel: `futures.${filter.channel}`,
65
- payload: [symbol, '20', '0'],
121
+ payload: filter.channel === 'obu' ? [`ob.${symbol}.${this.orderBookDepth}`] : [symbol, '20', '0'],
66
122
  time: Math.floor(new Date().valueOf() / 1000)
67
123
  }
68
124
  })
@@ -81,6 +137,22 @@ class GateIOFuturesSingleConnectionRealTimeFeed extends RealTimeFeedBase {
81
137
  return payload
82
138
  }
83
139
 
140
+ protected onMessage(message: any) {
141
+ if (message.channel !== 'futures.obu' || (message.event !== 'update' && message.event !== 'all')) {
142
+ return
143
+ }
144
+
145
+ const { result } = message
146
+ const previous = this.bookSequences.get(result.s)
147
+ if (
148
+ !Number.isSafeInteger(result.u) ||
149
+ (result.full !== true && (previous === undefined || result.U !== previous + 1 || result.u < result.U))
150
+ ) {
151
+ throw new Error(`Gate.io OBU sequence gap for ${result.s}: previous=${previous}, U=${result.U}, u=${result.u}`)
152
+ }
153
+ this.bookSequences.set(result.s, result.u)
154
+ }
155
+
84
156
  protected messageIsError(message: any): boolean {
85
157
  if (message.error !== null && message.error !== undefined) {
86
158
  return true
@@ -5,7 +5,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
5
5
  import { createDebug, type DebugLogger } from '../debug.ts'
6
6
  import { getProxyAgent, HttpClientError, ONE_SEC_IN_MS, optimizeFilters } from '../handy.ts'
7
7
  import { getMessageParser } from '../parsemessage.ts'
8
- import { createManagedRealTimeIterator, mergeRealTime, type ManagedRealTimeIterator } from '../realtimeiterator.ts'
8
+ import { closeIterator, createManagedRealTimeIterator, mergeRealTime, type ManagedRealTimeIterator } from '../realtimeiterator.ts'
9
9
  import { Exchange, Filter } from '../types.ts'
10
10
 
11
11
  export type RealTimeFeed = {
@@ -19,7 +19,7 @@ export type RealTimeFeed = {
19
19
 
20
20
  let connectionCounter = 1
21
21
 
22
- export type RealTimeFeedIterable = AsyncIterable<any>
22
+ export type RealTimeFeedIterable = { [Symbol.asyncIterator](): ManagedRealTimeIterator<any> }
23
23
 
24
24
  type RealTimeFeedConnection = {
25
25
  id: number
@@ -95,6 +95,7 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
95
95
  let staleConnectionTimerId: NodeJS.Timeout | undefined
96
96
  let pingTimerId: NodeJS.Timeout | undefined
97
97
  let connectionError: Error | undefined
98
+ let delivered = false
98
99
 
99
100
  try {
100
101
  this.manualSnapshotsBuffer.length = 0
@@ -173,6 +174,7 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
173
174
  this.onMessage(messageDeserialized)
174
175
 
175
176
  yield messageDeserialized
177
+ delivered = true
176
178
 
177
179
  if (retries > 0) {
178
180
  // reset retries counter as we've received correct message from the connection
@@ -216,33 +218,22 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
216
218
  if (connectionError !== undefined) {
217
219
  this._onError?.(connectionError)
218
220
  retries++
219
-
220
- const MAX_DELAY = 32 * 1000
221
- const httpError = connectionError instanceof HttpClientError ? connectionError : undefined
222
- const isRateLimited = httpError?.status === 418 || httpError?.status === 429 || connectionError.message.includes('429')
223
-
224
- if (isRateLimited) {
225
- const fallbackDelay = httpError?.status === 418 ? 2 * 60 * 1000 : (MAX_DELAY / 2) * retries
226
- retryDelay = httpError?.retryAfterMS ?? fallbackDelay
227
- } else {
228
- retryDelay = Math.min(Math.pow(2, retries - 1) * 1000, MAX_DELAY)
229
- }
221
+ retryDelay = getRetryDelay(connectionError, retries)
230
222
 
231
223
  this.debug(
232
- '(connection id: %d) %s real-time feed connection error, retries count: %d, next retry delay: %dms, rate limited: %s error message: %o',
224
+ '(connection id: %d) %s real-time feed connection error, retries count: %d, next retry delay: %dms, error message: %o',
233
225
  connectionId,
234
226
  this._exchange,
235
227
  retries,
236
228
  retryDelay,
237
- isRateLimited,
238
229
  connectionError
239
230
  )
240
231
  }
241
232
 
242
- yield { __disconnect__: true }
233
+ yield { __disconnect__: true, error: connectionError, delivered }
243
234
 
244
235
  if (retryDelay !== undefined) {
245
- await this._wait(retryDelay, this._closeController.signal)
236
+ await wait(retryDelay, this._closeController.signal)
246
237
  }
247
238
  }
248
239
  }
@@ -343,7 +334,7 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
343
334
 
344
335
  this._send(connection, message)
345
336
  if (this.throttleSubscribeMS > 0) {
346
- await this._wait(this.throttleSubscribeMS, connection.controller.signal)
337
+ await wait(this.throttleSubscribeMS, connection.controller.signal)
347
338
  }
348
339
  }
349
340
 
@@ -352,7 +343,7 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
352
343
  const messagesBeforeSnapshots = subscribeMessages.length === 0 ? 0 : subscribeMessages.length + 1
353
344
  // Wait for subscription acknowledgements and the first data message before fetching snapshots.
354
345
  while (shouldCancel() === false && connection.receivedMessagesCount < messagesBeforeSnapshots) {
355
- await this._wait(100, connection.controller.signal)
346
+ await wait(100, connection.controller.signal)
356
347
  }
357
348
 
358
349
  if (shouldCancel()) {
@@ -411,18 +402,31 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
411
402
  this.manualSnapshotsBuffer.length = 0
412
403
  }
413
404
  }
405
+ }
414
406
 
415
- private async _wait(delayMS: number, signal: AbortSignal) {
416
- try {
417
- await sleep(delayMS, undefined, { signal })
418
- } catch (error) {
419
- if (signal.aborted === false) {
420
- throw error
421
- }
407
+ async function wait(delayMS: number, signal: AbortSignal) {
408
+ try {
409
+ await sleep(delayMS, undefined, { signal })
410
+ } catch (error) {
411
+ if (signal.aborted === false) {
412
+ throw error
422
413
  }
423
414
  }
424
415
  }
425
416
 
417
+ function getRetryDelay(error: Error, retries: number) {
418
+ const MAX_DELAY = 32 * 1000
419
+ const httpError = error instanceof HttpClientError ? error : undefined
420
+ const isRateLimited = httpError?.status === 418 || httpError?.status === 429 || error.message.includes('429')
421
+
422
+ if (isRateLimited) {
423
+ const fallbackDelay = httpError?.status === 418 ? 2 * 60 * 1000 : (MAX_DELAY / 2) * retries
424
+ return httpError?.retryAfterMS ?? fallbackDelay
425
+ }
426
+
427
+ return Math.min(Math.pow(2, retries - 1) * 1000, MAX_DELAY)
428
+ }
429
+
426
430
  function resolveOrAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T | typeof ABORTED> {
427
431
  if (signal.aborted) {
428
432
  return Promise.resolve(ABORTED)
@@ -449,7 +453,12 @@ function resolveOrAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T
449
453
  })
450
454
  }
451
455
 
456
+ // A disconnect resets all normalized state, so when any connection drops, the feed closes all its connections and pollers,
457
+ // yields one disconnect and opens them again after the retry delay of the failed connection.
452
458
  export abstract class MultiConnectionRealTimeFeedBase implements RealTimeFeedIterable {
459
+ private readonly _closeController = new AbortController()
460
+ private _connections?: ManagedRealTimeIterator<any>
461
+
453
462
  constructor(
454
463
  private readonly _exchange: string,
455
464
  private readonly _filters: Filter<string>[],
@@ -458,8 +467,44 @@ export abstract class MultiConnectionRealTimeFeedBase implements RealTimeFeedIte
458
467
  ) {}
459
468
 
460
469
  [Symbol.asyncIterator](): ManagedRealTimeIterator<any> {
461
- const realTimeFeeds = Array.from(this._getRealTimeFeeds(this._exchange, this._filters, this._timeoutIntervalMS, this._onError))
462
- return mergeRealTime(realTimeFeeds)
470
+ return createManagedRealTimeIterator(this._stream(), () => {
471
+ this._closeController.abort()
472
+ return this._connections?.return()
473
+ })
474
+ }
475
+
476
+ private async *_stream() {
477
+ let retries = 0
478
+
479
+ while (this._closeController.signal.aborted === false) {
480
+ this._connections = mergeRealTime(
481
+ Array.from(this._getRealTimeFeeds(this._exchange, this._filters, this._timeoutIntervalMS, this._onError), untilDisconnect)
482
+ )
483
+ let disconnect: { error?: Error; delivered?: boolean } | undefined
484
+
485
+ for await (const message of this._connections) {
486
+ if (message.__disconnect__ === true) {
487
+ disconnect = message
488
+ break
489
+ }
490
+ yield message
491
+ }
492
+
493
+ if (disconnect === undefined) {
494
+ return
495
+ }
496
+
497
+ yield { __disconnect__: true }
498
+
499
+ if (disconnect.delivered === true) {
500
+ retries = 0
501
+ }
502
+
503
+ if (disconnect.error !== undefined) {
504
+ retries++
505
+ await wait(getRetryDelay(disconnect.error, retries), this._closeController.signal)
506
+ }
507
+ }
463
508
  }
464
509
 
465
510
  protected abstract _getRealTimeFeeds(
@@ -470,6 +515,22 @@ export abstract class MultiConnectionRealTimeFeedBase implements RealTimeFeedIte
470
515
  ): IterableIterator<RealTimeFeedIterable>
471
516
  }
472
517
 
518
+ // A connection stops at its disconnect, so it cannot reconnect on its own before the feed restarts all connections.
519
+ function untilDisconnect(feed: RealTimeFeedIterable) {
520
+ const messages = feed[Symbol.asyncIterator]()
521
+
522
+ async function* stream() {
523
+ for await (const message of messages) {
524
+ yield message
525
+ if (message.__disconnect__ === true) {
526
+ return
527
+ }
528
+ }
529
+ }
530
+
531
+ return createManagedRealTimeIterator(stream(), () => closeIterator(messages))
532
+ }
533
+
473
534
  export abstract class PoolingClientBase implements RealTimeFeedIterable {
474
535
  protected readonly debug: DebugLogger
475
536
  private _tid: NodeJS.Timeout | undefined = undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardis-machine",
3
- "version": "18.4.2",
3
+ "version": "18.6.0",
4
4
  "engines": {
5
5
  "node": ">=24.19"
6
6
  },
@@ -76,7 +76,7 @@
76
76
  "license": "MPL-2.0",
77
77
  "dependencies": {
78
78
  "debug": "^4.4.3",
79
- "tardis-dev": "^18.4.2",
79
+ "tardis-dev": "^18.6.0",
80
80
  "ws": "^8.21.3"
81
81
  },
82
82
  "bundleDependencies": true,
@@ -439,9 +439,20 @@ const gateIOFuturesMapper: SubscriptionMapper = {
439
439
  },
440
440
 
441
441
  map: (message: any) => {
442
+ const channel = message.channel.split('.')[1]
443
+ if (channel === 'obu') {
444
+ return [{ channel, symbols: message.payload.map((stream: string) => stream.slice(3, stream.lastIndexOf('.'))) }]
445
+ }
446
+ if (channel === 'order_book') {
447
+ return [{ channel, symbols: [message.payload[0]] }]
448
+ }
449
+ if (channel === 'public_liquidates' && message.payload.includes('!all')) {
450
+ return [{ channel, symbols: [] }]
451
+ }
452
+
442
453
  return [
443
454
  {
444
- channel: message.channel.split('.')[1],
455
+ channel,
445
456
  symbols: message.payload.map((s: any) => {
446
457
  if (typeof s === 'string') {
447
458
  return s