tardis-machine 18.5.1 → 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 (26) hide show
  1. package/node_modules/tardis-dev/dist/downloaddatasets.d.ts.map +1 -1
  2. package/node_modules/tardis-dev/dist/downloaddatasets.js +10 -4
  3. package/node_modules/tardis-dev/dist/downloaddatasets.js.map +1 -1
  4. package/node_modules/tardis-dev/dist/handy.d.ts +2 -1
  5. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  6. package/node_modules/tardis-dev/dist/handy.js +16 -7
  7. package/node_modules/tardis-dev/dist/handy.js.map +1 -1
  8. package/node_modules/tardis-dev/dist/mappers/bitflyer.js +3 -3
  9. package/node_modules/tardis-dev/dist/mappers/bitflyer.js.map +1 -1
  10. package/node_modules/tardis-dev/dist/mappers/bitget.js +9 -9
  11. package/node_modules/tardis-dev/dist/mappers/bitget.js.map +1 -1
  12. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.d.ts.map +1 -1
  13. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js +3 -11
  14. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js.map +1 -1
  15. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts +6 -2
  16. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  17. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +73 -26
  18. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  19. package/node_modules/tardis-dev/package.json +1 -1
  20. package/node_modules/tardis-dev/src/downloaddatasets.ts +13 -4
  21. package/node_modules/tardis-dev/src/handy.ts +17 -7
  22. package/node_modules/tardis-dev/src/mappers/bitflyer.ts +3 -3
  23. package/node_modules/tardis-dev/src/mappers/bitget.ts +9 -9
  24. package/node_modules/tardis-dev/src/realtimefeeds/coinbasederivatives.ts +3 -11
  25. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +89 -28
  26. package/package.json +2 -2
@@ -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.5.1",
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.5.1",
79
+ "tardis-dev": "^18.6.0",
80
80
  "ws": "^8.21.3"
81
81
  },
82
82
  "bundleDependencies": true,