tardis-dev 16.6.3 → 17.1.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.
@@ -2,36 +2,37 @@ import { Filter } from '../types.ts'
2
2
  import { RealTimeFeedBase } from './realtimefeed.ts'
3
3
 
4
4
  export class GeminiRealTimeFeed extends RealTimeFeedBase {
5
- protected wssURL = 'wss://api.gemini.com/v2/marketdata'
5
+ protected wssURL = 'wss://ws.gemini.com?snapshot=-1'
6
+ private readonly channels = new Set(['trade', 'depth', 'bookTicker'])
6
7
 
7
8
  protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
8
- const symbols = filters
9
- .map((filter) => {
10
- if (!filter.symbols || filter.symbols.length === 0) {
9
+ const params = filters
10
+ .flatMap((filter) => {
11
+ if (!this.channels.has(filter.channel)) {
12
+ throw new Error(`GeminiRealTimeFeed unsupported channel ${filter.channel}`)
13
+ }
14
+
15
+ if (!Array.isArray(filter.symbols) || filter.symbols.length === 0) {
11
16
  throw new Error('GeminiRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
12
17
  }
13
18
 
14
- return filter.symbols
19
+ const channel = filter.channel === 'depth' ? 'depth@100ms' : filter.channel
20
+ return filter.symbols.map((symbol) => `${symbol.toLowerCase()}@${channel}`)
15
21
  })
16
- .flatMap((s) => s)
17
22
  .filter((value, index, self) => {
18
23
  return self.indexOf(value) === index
19
24
  })
20
25
 
21
26
  return [
22
27
  {
23
- type: 'subscribe',
24
- subscriptions: [
25
- {
26
- name: 'l2',
27
- symbols
28
- }
29
- ]
28
+ method: 'SUBSCRIBE',
29
+ params,
30
+ id: 1
30
31
  }
31
32
  ]
32
33
  }
33
34
 
34
35
  protected messageIsError(message: any): boolean {
35
- return message.result === 'error'
36
+ return message.result === 'error' || message.error !== undefined || message.status >= 400
36
37
  }
37
38
  }
package/src/replay.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { createReadStream } from 'node:fs'
2
2
  import { rm } from 'node:fs/promises'
3
- import { EventEmitter } from 'events'
3
+ import { EventEmitter, once } from 'events'
4
4
  import { Worker } from 'worker_threads'
5
5
  import { constants, createGunzip, createZstdDecompress } from 'zlib'
6
- import { BinarySplitStream } from './binarysplit.ts'
6
+ import { BinarySplitBatchStream } from './binarysplit.ts'
7
7
  import { clearCacheSync } from './clearcache.ts'
8
8
  import { EXCHANGES, EXCHANGE_CHANNELS_INFO } from './consts.ts'
9
9
  import { debug } from './debug.ts'
10
- import { addDays, createNormalizedSymbolFilter, getFilters, normalizeMessages, parseAsUTCDate, wait } from './handy.ts'
11
- import { MapperFactory, normalizeBookChanges } from './mappers/index.ts'
10
+ import { addDays, createNormalizedSymbolFilter, getFilters, parseAsUTCDate, wait } from './handy.ts'
11
+ import { Mapper, MapperFactory, normalizeBookChanges } from './mappers/index.ts'
12
12
  import { getOptions } from './options.ts'
13
13
  import { Disconnect, Exchange, FilterForExchange } from './types.ts'
14
14
  import { WorkerJobPayload, WorkerMessage, WorkerSignal } from './worker.ts'
@@ -18,6 +18,19 @@ type ReplayNormalizedMessage<U extends readonly MapperFactory<any, any>[], Z ext
18
18
  ? MapperOutput<U[number]> | Disconnect
19
19
  : MapperOutput<U[number]>
20
20
 
21
+ const DATE_MESSAGE_SPLIT_INDEX = 28
22
+ const CHUNK_SIZE = 256 * 1024
23
+ // Keep decompression lenient for partial gzip responses.
24
+ // See https://github.com/request/request/pull/2492 and https://github.com/node-fetch/node-fetch/pull/239.
25
+ const GZIP_OPTIONS = {
26
+ chunkSize: CHUNK_SIZE,
27
+ flush: constants.Z_SYNC_FLUSH,
28
+ finishFlush: constants.Z_SYNC_FLUSH
29
+ }
30
+
31
+ type DecodedReplayMessage = { localTimestamp: Date; message: any }
32
+ type ReplayMessage = DecodedReplayMessage | { localTimestamp: Buffer; message: Buffer } | undefined
33
+
21
34
  export async function* replay<T extends Exchange, U extends boolean = false, Z extends boolean = false>({
22
35
  exchange,
23
36
  from,
@@ -38,6 +51,58 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
38
51
  ? { localTimestamp: Buffer; message: Buffer }
39
52
  : { localTimestamp: Date; message: any }
40
53
  > {
54
+ let lastMessageWasUndefined = false
55
+
56
+ const lineBatches = replayLineBatches({ exchange, from, to, filters, apiKey, autoCleanup, waitWhenDataNotYetAvailable })
57
+ for await (const bufferLines of lineBatches) {
58
+ // Decode one line batch in a tight loop, then preserve the public one-message-at-a-time iterator.
59
+ const messages: ReplayMessage[] = []
60
+
61
+ for (let i = 0; i < bufferLines.length; i++) {
62
+ const bufferLine = bufferLines[i]
63
+ if (bufferLine.length > 0) {
64
+ lastMessageWasUndefined = false
65
+ if (skipDecoding === true) {
66
+ messages.push({
67
+ localTimestamp: bufferLine.slice(0, DATE_MESSAGE_SPLIT_INDEX),
68
+ message: bufferLine.slice(DATE_MESSAGE_SPLIT_INDEX + 1)
69
+ })
70
+ } else {
71
+ const message = parseReplayMessage(exchange, bufferLine)
72
+ const localTimestamp = parseReplayTimestamp(bufferLine)
73
+ if (withMicroseconds) {
74
+ localTimestamp.μs = parseReplayMicroseconds(bufferLine)
75
+ }
76
+
77
+ messages.push({ localTimestamp, message })
78
+ }
79
+ } else if (withDisconnects === true && lastMessageWasUndefined === false) {
80
+ lastMessageWasUndefined = true
81
+ messages.push(undefined)
82
+ }
83
+ }
84
+
85
+ // Drain a batch already received from the stream; its iterator surfaces later stream errors on the next read.
86
+ for (let i = 0; i < messages.length; i++) {
87
+ yield messages[i] as any
88
+ }
89
+ }
90
+ }
91
+
92
+ type ReplayLineOptions<T extends Exchange> = Pick<
93
+ ReplayOptions<T, boolean, boolean>,
94
+ 'exchange' | 'from' | 'to' | 'filters' | 'apiKey' | 'autoCleanup' | 'waitWhenDataNotYetAvailable'
95
+ >
96
+
97
+ async function* replayLineBatches<T extends Exchange>({
98
+ exchange,
99
+ from,
100
+ to,
101
+ filters,
102
+ apiKey = undefined,
103
+ autoCleanup = undefined,
104
+ waitWhenDataNotYetAvailable = undefined
105
+ }: ReplayLineOptions<T>): AsyncIterableIterator<Buffer[]> {
41
106
  validateReplayOptions(exchange, from, to, filters)
42
107
 
43
108
  const fromDate = parseAsUTCDate(from)
@@ -78,23 +143,6 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
78
143
  })
79
144
 
80
145
  try {
81
- // date is always formatted to have length of 28 so we can skip looking for first space in line and use it
82
- // as hardcoded value
83
- const DATE_MESSAGE_SPLIT_INDEX = 28
84
-
85
- // more lenient gzip decompression
86
- // see https://github.com/request/request/pull/2492 and https://github.com/node-fetch/node-fetch/pull/239
87
-
88
- const CHUNK_SIZE = 256 * 1024
89
- const GZIP_OPTIONS = {
90
- chunkSize: CHUNK_SIZE,
91
- flush: constants.Z_SYNC_FLUSH,
92
- finishFlush: constants.Z_SYNC_FLUSH
93
- }
94
-
95
- // helper flag that helps us not yielding two subsequent undefined/disconnect messages
96
- let lastMessageWasUndefined = false
97
-
98
146
  let currentSliceDate = new Date(fromDate)
99
147
  // iterate over every minute in <=from,to> date range
100
148
  // get cached slice paths, read them as file streams, decompress, split by new lines and yield as messages
@@ -113,9 +161,9 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
113
161
  }
114
162
 
115
163
  if (cachedSlice === undefined) {
116
- // if response for requested date is not ready yet wait 100ms and try again
164
+ // if the requested slice is not ready yet, wait for the worker to report another cached slice
117
165
  debug('waiting for slice: %s, exchange: %s', sliceKey, exchange)
118
- await wait(100)
166
+ await once(worker, 'message')
119
167
  }
120
168
  }
121
169
 
@@ -130,7 +178,7 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
130
178
  linesStream.destroy(err)
131
179
  })
132
180
  // and split by new line
133
- .pipe(new BinarySplitStream())
181
+ .pipe(new BinarySplitBatchStream())
134
182
  .on('error', function onBinarySplitStreamError(err) {
135
183
  debug('binary split stream error %o', err)
136
184
  linesStream.destroy(err)
@@ -138,47 +186,9 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
138
186
 
139
187
  let linesCount = 0
140
188
 
141
- for await (const bufferLine of linesStream as unknown as Iterable<Buffer>) {
142
- linesCount++
143
- if (bufferLine.length > 0) {
144
- lastMessageWasUndefined = false
145
- // as any due to https://github.com/Microsoft/TypeScript/issues/24929
146
- if (skipDecoding === true) {
147
- yield {
148
- localTimestamp: bufferLine.slice(0, DATE_MESSAGE_SPLIT_INDEX),
149
- message: bufferLine.slice(DATE_MESSAGE_SPLIT_INDEX + 1)
150
- } as any
151
- } else {
152
- let messageString = bufferLine.toString('utf8', DATE_MESSAGE_SPLIT_INDEX + 1)
153
-
154
- // hack to handle huobi long numeric id for trades
155
- if (exchange.startsWith('huobi-') && messageString.includes('.trade.detail')) {
156
- messageString = messageString.replace(/"id":([0-9]+),/g, '"id":"$1",')
157
- }
158
- // hack to handle upbit long numeric id for trades
159
- if (exchange === 'upbit' && messageString.includes('sequential_id')) {
160
- messageString = messageString.replace(/"sequential_id":([0-9]+),/g, '"sequential_id":"$1",')
161
- }
162
-
163
- const message = JSON.parse(messageString)
164
-
165
- const localTimestamp = new Date(bufferLine.toString('utf8', 0, DATE_MESSAGE_SPLIT_INDEX))
166
- if (withMicroseconds) {
167
- localTimestamp.μs = parseReplayMicroseconds(bufferLine)
168
- }
169
-
170
- yield {
171
- // when skipDecoding is not set, decode timestamp to Date and message to object
172
- localTimestamp,
173
- message
174
- } as any
175
- }
176
- // ignore empty lines unless withDisconnects is set to true
177
- // do not yield subsequent undefined messages
178
- } else if (withDisconnects === true && lastMessageWasUndefined === false) {
179
- lastMessageWasUndefined = true
180
- yield undefined as any
181
- }
189
+ for await (const bufferLines of linesStream as AsyncIterable<Buffer[]>) {
190
+ linesCount += bufferLines.length
191
+ yield bufferLines
182
192
  }
183
193
 
184
194
  debug('processed slice: %s, exchange: %s, count: %d', sliceKey, exchange, linesCount)
@@ -232,6 +242,21 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
232
242
  }
233
243
  }
234
244
 
245
+ function parseReplayMessage(exchange: Exchange, bufferLine: Buffer) {
246
+ let messageString = bufferLine.toString('utf8', DATE_MESSAGE_SPLIT_INDEX + 1)
247
+
248
+ // hack to handle huobi long numeric id for trades
249
+ if (exchange.startsWith('huobi-') && messageString.includes('.trade.detail')) {
250
+ messageString = messageString.replace(/"id":([0-9]+),/g, '"id":"$1",')
251
+ }
252
+ // hack to handle upbit long numeric id for trades
253
+ if (exchange === 'upbit' && messageString.includes('sequential_id')) {
254
+ messageString = messageString.replace(/"sequential_id":([0-9]+),/g, '"sequential_id":"$1",')
255
+ }
256
+
257
+ return JSON.parse(messageString)
258
+ }
259
+
235
260
  async function cleanupSlice(slicePath: string) {
236
261
  try {
237
262
  await rm(slicePath, { force: true })
@@ -244,6 +269,29 @@ function parseReplayMicroseconds(bufferLine: Buffer) {
244
269
  return (bufferLine[23] - 48) * 100 + (bufferLine[24] - 48) * 10 + (bufferLine[25] - 48)
245
270
  }
246
271
 
272
+ function parseReplayTimestamp(bufferLine: Buffer) {
273
+ // Recorder writes yyyy-MM-ddTHH:mm:ss.fffffffZ.
274
+ return new Date(
275
+ Date.UTC(
276
+ parseReplayTwoDigits(bufferLine, 0) * 100 + parseReplayTwoDigits(bufferLine, 2),
277
+ parseReplayTwoDigits(bufferLine, 5) - 1,
278
+ parseReplayTwoDigits(bufferLine, 8),
279
+ parseReplayTwoDigits(bufferLine, 11),
280
+ parseReplayTwoDigits(bufferLine, 14),
281
+ parseReplayTwoDigits(bufferLine, 17),
282
+ parseReplayThreeDigits(bufferLine, 20)
283
+ )
284
+ )
285
+ }
286
+
287
+ function parseReplayTwoDigits(bufferLine: Buffer, start: number) {
288
+ return (bufferLine[start] - 48) * 10 + (bufferLine[start + 1] - 48)
289
+ }
290
+
291
+ function parseReplayThreeDigits(bufferLine: Buffer, start: number) {
292
+ return (bufferLine[start] - 48) * 100 + (bufferLine[start + 1] - 48) * 10 + (bufferLine[start + 2] - 48)
293
+ }
294
+
247
295
  // gracefully terminate worker
248
296
  async function terminateWorker(worker: Worker, waitTimeout: number) {
249
297
  let cancelWait = () => {}
@@ -279,9 +327,9 @@ export function replayNormalized<T extends Exchange, U extends MapperFactory<T,
279
327
 
280
328
  validateReplayNormalizedOptions(fromDate, normalizers)
281
329
 
282
- const createMappers = (localTimestamp: Date) => normalizers.map((m) => m(exchange, localTimestamp))
283
- const mappers = createMappers(fromDate)
284
- const filters = getFilters(mappers, symbols)
330
+ const createMappersAt = (localTimestamp: Date) => normalizers.map((normalizer) => normalizer(exchange, localTimestamp))
331
+ const initialMappers = createMappersAt(fromDate)
332
+ const filters = getFilters(initialMappers, symbols)
285
333
 
286
334
  // filter normalized messages by symbol as some exchanges do not provide server side filtering so we could end up with messages
287
335
  // for symbols we've not requested for
@@ -289,19 +337,17 @@ export function replayNormalized<T extends Exchange, U extends MapperFactory<T,
289
337
 
290
338
  if (segments.length <= 1) {
291
339
  const filter = createNormalizedSymbolFilter(symbols, filters)
292
- const messages = replay({
340
+ const lineBatches = replayLineBatches({
293
341
  exchange,
294
342
  from,
295
343
  to,
296
- withDisconnects: true,
297
344
  filters,
298
345
  apiKey,
299
- withMicroseconds: true,
300
346
  autoCleanup,
301
347
  waitWhenDataNotYetAvailable
302
348
  })
303
349
 
304
- return normalizeMessages(exchange, undefined, messages, mappers, createMappers, withDisconnectMessages, filter)
350
+ return normalizeReplayLineBatches(exchange, undefined, lineBatches, initialMappers, createMappersAt, withDisconnectMessages, filter)
305
351
  }
306
352
 
307
353
  return replayNormalizedSegments()
@@ -309,23 +355,116 @@ export function replayNormalized<T extends Exchange, U extends MapperFactory<T,
309
355
  async function* replayNormalizedSegments() {
310
356
  for (let i = 0; i < segments.length; i++) {
311
357
  const { from: segmentFrom, to: segmentTo } = segments[i]
312
- const segmentMappers = i === 0 ? mappers : createMappers(segmentFrom)
358
+ const segmentMappers = i === 0 ? initialMappers : createMappersAt(segmentFrom)
313
359
  const segmentFilters = getFilters(segmentMappers, symbols)
314
360
  const segmentFilter = createNormalizedSymbolFilter(symbols, segmentFilters)
315
361
 
316
- const segmentMessages = replay({
362
+ const segmentLineBatches = replayLineBatches({
317
363
  exchange,
318
364
  from: segmentFrom.toISOString(),
319
365
  to: segmentTo.toISOString(),
320
- withDisconnects: true,
321
366
  filters: segmentFilters,
322
367
  apiKey,
323
- withMicroseconds: true,
324
368
  autoCleanup,
325
369
  waitWhenDataNotYetAvailable
326
370
  })
327
371
 
328
- yield* normalizeMessages(exchange, undefined, segmentMessages, segmentMappers, createMappers, withDisconnectMessages, segmentFilter)
372
+ yield* normalizeReplayLineBatches(
373
+ exchange,
374
+ undefined,
375
+ segmentLineBatches,
376
+ segmentMappers,
377
+ createMappersAt,
378
+ withDisconnectMessages,
379
+ segmentFilter
380
+ )
381
+ }
382
+ }
383
+ }
384
+
385
+ async function* normalizeReplayLineBatches(
386
+ exchange: Exchange,
387
+ symbols: string[] | undefined,
388
+ lineBatches: AsyncIterableIterator<Buffer[]>,
389
+ initialMappers: Mapper<any, any>[],
390
+ createMappersAt: (localTimestamp: Date) => Mapper<any, any>[],
391
+ withDisconnectMessages: boolean | undefined,
392
+ filter?: (symbol: string) => boolean
393
+ ) {
394
+ // This intentionally keeps mapper calls lazy. Custom normalizers must not process later raw messages before the consumer asks for them.
395
+ let previousLocalTimestamp: Date | undefined
396
+ let activeMappers: Mapper<any, any>[] | undefined = initialMappers
397
+ if (activeMappers.length === 0) {
398
+ throw new Error(`Can't normalize data without any normalizers provided`)
399
+ }
400
+
401
+ for await (const bufferLines of lineBatches) {
402
+ const decodedMessages: (DecodedReplayMessage | undefined)[] = []
403
+ let decodingFailed = false
404
+ let decodingError: unknown
405
+ try {
406
+ for (let i = 0; i < bufferLines.length; i++) {
407
+ const bufferLine = bufferLines[i]
408
+ if (bufferLine.length === 0) {
409
+ decodedMessages.push(undefined)
410
+ } else {
411
+ const message = parseReplayMessage(exchange, bufferLine)
412
+ const localTimestamp = parseReplayTimestamp(bufferLine)
413
+ localTimestamp.μs = parseReplayMicroseconds(bufferLine)
414
+ decodedMessages.push({ localTimestamp, message })
415
+ }
416
+ }
417
+ } catch (error) {
418
+ decodingFailed = true
419
+ decodingError = error
420
+ }
421
+
422
+ for (let i = 0; i < decodedMessages.length; i++) {
423
+ const decodedMessage = decodedMessages[i]
424
+ if (decodedMessage === undefined) {
425
+ if (activeMappers === undefined) {
426
+ continue
427
+ }
428
+
429
+ activeMappers = undefined
430
+ if (withDisconnectMessages === true && previousLocalTimestamp !== undefined) {
431
+ const disconnect: Disconnect = {
432
+ type: 'disconnect',
433
+ exchange,
434
+ localTimestamp: previousLocalTimestamp,
435
+ symbols
436
+ }
437
+ yield disconnect as any
438
+ }
439
+ continue
440
+ }
441
+
442
+ const { localTimestamp, message } = decodedMessage
443
+
444
+ if (activeMappers === undefined) {
445
+ activeMappers = createMappersAt(localTimestamp)
446
+ }
447
+ previousLocalTimestamp = localTimestamp
448
+
449
+ for (let mapperIndex = 0; mapperIndex < activeMappers.length; mapperIndex++) {
450
+ const mapper = activeMappers[mapperIndex]
451
+ if (mapper.canHandle(message)) {
452
+ const mappedMessages = mapper.map(message, localTimestamp)
453
+ if (!mappedMessages) {
454
+ continue
455
+ }
456
+
457
+ for (const normalizedMessage of mappedMessages) {
458
+ if (filter === undefined || filter(normalizedMessage.symbol)) {
459
+ yield normalizedMessage
460
+ }
461
+ }
462
+ }
463
+ }
464
+ }
465
+
466
+ if (decodingFailed) {
467
+ throw decodingError
329
468
  }
330
469
  }
331
470
  }