tardis-dev 16.6.2 → 17.0.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.
@@ -41,6 +41,9 @@ function isExcludedFromNormalizedOutput(symbol: string) {
41
41
  return symbol === 'sOLUSDT'
42
42
  }
43
43
 
44
+ // Uppercase normalized spot symbols can be indistinguishable from native Phemex perpetual symbols.
45
+ // Keep this list aligned with tardis-api and tardis-exporter so aliases such as SBTCUSDT can be
46
+ // converted back to sBTCUSDT without changing native perpetual symbols such as SPKUSDT.
44
47
  const COINS_STARTING_WITH_S = [
45
48
  'SOLUSD',
46
49
  'SUSHIUSD',
@@ -122,13 +125,46 @@ const COINS_STARTING_WITH_S = [
122
125
  'STXXUSDT',
123
126
  'SPYXUSDT',
124
127
  'SP500USDT',
125
- 'SNDKUSDT'
128
+ 'SNDKUSDT',
129
+ 'SKHYUSDT',
130
+ 'SMCIUSDT',
131
+ 'SONYUSDT',
132
+ 'SQQQUSDT',
133
+ 'STRCUSDT'
126
134
  ]
135
+
136
+ // Phemex used the V2 `_p` channels for this short-lived, now delisted PerpetualPilot market family.
137
+ // Exact casing matters: SIRENPUSD was a pilot, while sIRENPUSD was a separate spot instrument.
138
+ const PERPETUAL_PILOT_SYMBOLS = new Set([
139
+ 'ANGLERFISHPUSD',
140
+ 'BNBCARDPUSD',
141
+ 'BNBXBTPUSD',
142
+ 'BUTTCOINPUSD',
143
+ 'CRYPTOAIPUSD',
144
+ 'CTDPUSD',
145
+ 'DOGEAIPUSD',
146
+ 'FULLSENDPUSD',
147
+ 'GHIBLIPUSD',
148
+ 'GREED3PUSD',
149
+ 'IMGPUSD',
150
+ 'MCPOSPUSD',
151
+ 'PAINPUSD',
152
+ 'PERRYPUSD',
153
+ 'SIRENPUSD',
154
+ 'TITCOINPUSD',
155
+ 'TOLYPUSD',
156
+ 'WILDNOUTPUSD'
157
+ ])
158
+
127
159
  function getInstrumentType(symbol: string) {
128
160
  if (/\d+$/.test(symbol)) {
129
161
  return 'future'
130
162
  }
131
163
 
164
+ if (symbol.startsWith('s')) {
165
+ return 'spot'
166
+ }
167
+
132
168
  if (
133
169
  COINS_STARTING_WITH_S.some((coin) => (coin === 'SUSDT' ? symbol === coin : symbol.startsWith(coin))) ||
134
170
  symbol.startsWith('S') === false
@@ -140,6 +176,16 @@ function getInstrumentType(symbol: string) {
140
176
  }
141
177
 
142
178
  function getApiSymbolId(symbolId: string) {
179
+ // These are already exchange-native mixed-case identifiers.
180
+ if (symbolId.startsWith('s') || symbolId.startsWith('u100') || symbolId.startsWith('c')) {
181
+ return symbolId
182
+ }
183
+
184
+ // SIRENPUSD must not be mistaken for the uppercase alias of spot sIRENPUSD.
185
+ if (PERPETUAL_PILOT_SYMBOLS.has(symbolId)) {
186
+ return symbolId
187
+ }
188
+
143
189
  const type = getInstrumentType(symbolId)
144
190
  if (type === 'spot' && symbolId.startsWith('S')) {
145
191
  return symbolId.charAt(0).toLowerCase() + symbolId.slice(1)
@@ -155,13 +201,26 @@ function getApiSymbolId(symbolId: string) {
155
201
  return symbolId
156
202
  }
157
203
 
158
- function getSymbols(symbols: string[]) {
159
- const perpV2Symbols = symbols.filter((s) => getInstrumentType(s) === 'perpetual' && s.endsWith('USDT')).map(getApiSymbolId)
160
- const otherSymbols = symbols.filter((s) => getInstrumentType(s) !== 'perpetual' || s.endsWith('USDT') == false).map(getApiSymbolId)
204
+ function splitSymbolsByChannelFamily(symbols: string[]) {
205
+ const v2Symbols: string[] = []
206
+ const legacySymbols: string[] = []
207
+
208
+ for (const symbol of symbols) {
209
+ const apiSymbolId = getApiSymbolId(symbol)
210
+ const usesV2Channels =
211
+ PERPETUAL_PILOT_SYMBOLS.has(apiSymbolId) ||
212
+ (getInstrumentType(symbol) === 'perpetual' && (apiSymbolId.endsWith('USDT') || apiSymbolId.endsWith('USDC')))
213
+
214
+ if (usesV2Channels) {
215
+ v2Symbols.push(apiSymbolId)
216
+ } else {
217
+ legacySymbols.push(apiSymbolId)
218
+ }
219
+ }
161
220
 
162
221
  return {
163
- perpV2Symbols,
164
- otherSymbols
222
+ v2Symbols,
223
+ legacySymbols
165
224
  }
166
225
  }
167
226
 
@@ -182,20 +241,20 @@ const phemexTradesMapper: Mapper<'phemex', Trade> = {
182
241
  ]
183
242
  }
184
243
 
185
- const { perpV2Symbols, otherSymbols } = getSymbols(symbols)
244
+ const { v2Symbols, legacySymbols } = splitSymbolsByChannelFamily(symbols)
186
245
 
187
246
  const filters = []
188
247
 
189
- if (perpV2Symbols.length > 0) {
248
+ if (v2Symbols.length > 0) {
190
249
  filters.push({
191
250
  channel: 'trades_p',
192
- symbols: perpV2Symbols
251
+ symbols: v2Symbols
193
252
  } as const)
194
253
  }
195
- if (otherSymbols.length > 0) {
254
+ if (legacySymbols.length > 0) {
196
255
  filters.push({
197
256
  channel: 'trades',
198
- symbols: otherSymbols
257
+ symbols: legacySymbols
199
258
  } as const)
200
259
  }
201
260
 
@@ -273,19 +332,19 @@ const phemexBookChangeMapper: Mapper<'phemex', BookChange> = {
273
332
  ]
274
333
  }
275
334
 
276
- const { perpV2Symbols, otherSymbols } = getSymbols(symbols)
335
+ const { v2Symbols, legacySymbols } = splitSymbolsByChannelFamily(symbols)
277
336
  const filters = []
278
337
 
279
- if (perpV2Symbols.length > 0) {
338
+ if (v2Symbols.length > 0) {
280
339
  filters.push({
281
340
  channel: 'orderbook_p',
282
- symbols: perpV2Symbols
341
+ symbols: v2Symbols
283
342
  } as const)
284
343
  }
285
- if (otherSymbols.length > 0) {
344
+ if (legacySymbols.length > 0) {
286
345
  filters.push({
287
346
  channel: 'book',
288
- symbols: otherSymbols
347
+ symbols: legacySymbols
289
348
  } as const)
290
349
  }
291
350
 
@@ -345,19 +404,19 @@ class PhemexDerivativeTickerMapper implements Mapper<'phemex', DerivativeTicker>
345
404
  ]
346
405
  }
347
406
 
348
- const { perpV2Symbols, otherSymbols } = getSymbols(symbols)
407
+ const { v2Symbols, legacySymbols } = splitSymbolsByChannelFamily(symbols)
349
408
  const filters = []
350
409
 
351
- if (perpV2Symbols.length > 0) {
410
+ if (v2Symbols.length > 0) {
352
411
  filters.push({
353
412
  channel: 'perp_market24h_pack_p',
354
- symbols: perpV2Symbols
413
+ symbols: v2Symbols
355
414
  } as const)
356
415
  }
357
- if (otherSymbols.length > 0) {
416
+ if (legacySymbols.length > 0) {
358
417
  filters.push({
359
418
  channel: 'market24h',
360
- symbols: otherSymbols
419
+ symbols: legacySymbols
361
420
  } as const)
362
421
  }
363
422
 
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
  }