UNPKG

@hackape/tardis-dev

Version:

Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js

117 lines (95 loc) 3.45 kB
import { PassThrough } from 'stream' import { once } from 'events' type NextMessageResultWitIndex = { index: number result: IteratorResult<Combinable, Combinable> } type Combinable = { localTimestamp: Date } const DATE_MAX = new Date(8640000000000000) async function nextWithIndex(iterator: AsyncIterableIterator<Combinable>, index: number): Promise<NextMessageResultWitIndex> { const result = await iterator.next() return { result, index } } function findOldestResult(oldest: NextMessageResultWitIndex, current: NextMessageResultWitIndex) { if (oldest.result.done) { return oldest } if (current.result.done) { return current } const currentTimestamp = current.result.value.localTimestamp.valueOf() const oldestTimestamp = oldest.result.value.localTimestamp.valueOf() if (currentTimestamp < oldestTimestamp) { return current } if (currentTimestamp === oldestTimestamp) { const currentTimestampMicroSeconds = current.result.value.localTimestamp.μs || 0 const oldestTimestampMicroSeconds = oldest.result.value.localTimestamp.μs || 0 if (currentTimestampMicroSeconds < oldestTimestampMicroSeconds) { return current } } return oldest } // combines multiple iterators from for example multiple exchanges // works both for real-time and historical data export async function* combine<T extends AsyncIterableIterator<Combinable>[]>( ...iterators: T ): AsyncIterableIterator<T extends AsyncIterableIterator<infer U>[] ? U : never> { if (iterators.length === 0) { return } // decide based on first provided iterator if we're dealing with real-time or historical data streams if ((iterators[0] as any).__realtime__) { const combinedStream = new PassThrough({ objectMode: true, highWaterMark: 8096 }) iterators.forEach(async function writeMessagesToCombinedStream(messages) { for await (const message of messages) { if (combinedStream.destroyed) { return } if (!combinedStream.write(message)) // Handle backpressure on write await once(combinedStream, 'drain') } }) for await (const message of combinedStream) { yield message } } else { return yield* combineHistorical(iterators) as any } } async function* combineHistorical(iterators: AsyncIterableIterator<Combinable>[]) { // wait for all results to resolve const results = await Promise.all(iterators.map(nextWithIndex)) let aliveIteratorsCount = results.length do { // if we're deailing with historical data replay // and need to return combined messages iterable sorted by local timestamp in acending order // find resolved one that is the 'oldest' const oldestResult = results.reduce(findOldestResult, results[0]) const { result, index } = oldestResult if (result.done) { aliveIteratorsCount-- // we don't want finished iterators to every be considered 'oldest' again // hence provide them with result that has local timestamp set to DATE_MAX // and that is not done results[index].result = { done: false, value: { localTimestamp: DATE_MAX } } } else { // yield oldest value and replace with next value from iterable for given index yield result.value results[index] = await nextWithIndex(iterators[index], index) } } while (aliveIteratorsCount > 0) }