@hackape/tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
162 lines (140 loc) • 5.26 kB
text/typescript
import { debug } from './debug'
import { getFilters, normalizeMessages } from './handy'
import { MapperFactory } from './mappers'
import { createRealTimeFeed } from './realtimefeeds'
import { Disconnect, Exchange, Filter, FilterForExchange } from './types'
async function* _stream<T extends Exchange, U extends boolean = false>({
exchange,
filters,
timeoutIntervalMS = 10000,
withDisconnects = undefined,
onError = undefined
}: StreamOptions<T, U>): AsyncIterableIterator<
U extends true ? { localTimestamp: Date; message: any } | undefined : { localTimestamp: Date; message: any }
> {
validateStreamOptions(filters)
const realTimeFeed = createRealTimeFeed(exchange, filters, timeoutIntervalMS, onError)
for await (const message of realTimeFeed) {
if (message === undefined) {
// undefined message means that websocket connection has been closed
// notify about it by yielding undefined if flag is set
if (withDisconnects) {
yield undefined as any
}
} else {
yield {
localTimestamp: new Date(),
message
} as any
}
}
}
export function stream<T extends Exchange, U extends boolean = false>({
exchange,
filters,
timeoutIntervalMS = 10000,
withDisconnects = undefined,
onError = undefined
}: StreamOptions<T, U>): AsyncIterableIterator<
U extends true ? { localTimestamp: Date; message: any } | undefined : { localTimestamp: Date; message: any }
> {
let _iterator = _stream({ exchange, filters, timeoutIntervalMS, withDisconnects, onError })
;(_iterator as any).__realtime__ = true
return _iterator
}
async function* _streamNormalized<T extends Exchange, U extends MapperFactory<T, any>[], Z extends boolean = false>(
{ exchange, symbols, timeoutIntervalMS = 10000, withDisconnectMessages = undefined, onError = undefined }: StreamNormalizedOptions<T, Z>,
...normalizers: U
): AsyncIterableIterator<
Z extends true
? U extends MapperFactory<infer _, infer X>[]
? X | Disconnect
: never
: U extends MapperFactory<infer _, infer X>[]
? X
: never
> {
// mappers assume that symbols are uppercased by default
// if user by mistake provide lowercase one let's automatically fix it
if (symbols !== undefined) {
symbols = symbols.map((s) => s.toUpperCase())
}
while (true) {
try {
const createMappers = (localTimestamp: Date) => normalizers.map((m) => m(exchange, localTimestamp))
const mappers = createMappers(new Date())
const filters = getFilters(mappers, symbols)
const messages = _stream({
exchange,
withDisconnects: true,
timeoutIntervalMS,
filters,
onError
})
// filter normalized messages by symbol as some exchanges do not offer subscribing to specific symbols for some of the channels
// for example Phemex market24h channel
const filter = (symbol: string) => {
return symbols === undefined || symbols.length === 0 || symbols.includes(symbol)
}
const normalizedMessages = normalizeMessages(exchange, messages, mappers, createMappers, withDisconnectMessages, filter, new Date())
for await (const message of normalizedMessages) {
yield message
}
} catch (error) {
if (onError !== undefined) {
onError(error)
}
debug('%s normalize messages error: %o, retrying with new connection...', exchange, error)
if (withDisconnectMessages) {
// yield it as disconnect as well if flag is set
const disconnect: Disconnect = {
type: 'disconnect',
exchange,
localTimestamp: new Date()
}
yield disconnect as any
}
}
}
}
function validateStreamOptions(filters: Filter<string>[]) {
if (!filters) {
throw new Error(`Invalid "filters" argument. Please provide filters array`)
}
for (let i = 0; i < filters.length; i++) {
const filter = filters[i]
if (filter.symbols && Array.isArray(filter.symbols) === false) {
throw new Error(`Invalid "filters[].symbols" argument: ${filter.symbols}. Please provide array of symbol strings`)
}
}
}
export type StreamOptions<T extends Exchange, U extends boolean = false> = {
exchange: T
filters: FilterForExchange[T][]
timeoutIntervalMS?: number
withDisconnects?: U
onError?: (error: Error) => void
}
export type StreamNormalizedOptions<T extends Exchange, U extends boolean = false> = {
exchange: T
symbols?: string[]
timeoutIntervalMS?: number
withDisconnectMessages?: U
onError?: (error: Error) => void
}
export function streamNormalized<T extends Exchange, U extends MapperFactory<T, any>[], Z extends boolean = false>(
{ exchange, symbols, timeoutIntervalMS = 10000, withDisconnectMessages = undefined, onError = undefined }: StreamNormalizedOptions<T, Z>,
...normalizers: U
): AsyncIterableIterator<
Z extends true
? U extends MapperFactory<infer _, infer X>[]
? X | Disconnect
: never
: U extends MapperFactory<infer _, infer X>[]
? X
: never
> {
let _iterator = _streamNormalized({ exchange, symbols, timeoutIntervalMS, withDisconnectMessages, onError }, ...normalizers)
;(_iterator as any).__realtime__ = true
return _iterator
}