UNPKG

tardis-machine

Version:

Locally runnable server with built-in data caching, providing both tick-level historical and consolidated real-time cryptocurrency market data via HTTP and WebSocket APIs

138 lines (110 loc) 4.46 kB
import qs from 'querystring' import { combine, compute, Exchange, streamNormalized } from 'tardis-dev' import { HttpRequest, WebSocket } from 'uWebSockets.js' import { debug } from '../debug' import { constructDataTypeFilter, getComputables, getNormalizers, StreamNormalizedRequestOptions, wait } from '../helpers' export async function streamNormalizedWS(ws: any, req: HttpRequest) { let messages: AsyncIterableIterator<any> | undefined try { const startTimestamp = new Date().getTime() const parsedQuery = qs.decode(req.getQuery()) const optionsString = parsedQuery['options'] as string const streamNormalizedOptions = JSON.parse(optionsString) as StreamNormalizedRequestOptions debug('WebSocket /ws-stream-normalized started, options: %o', streamNormalizedOptions) const options = Array.isArray(streamNormalizedOptions) ? streamNormalizedOptions : [streamNormalizedOptions] let subSequentErrorsCount: { [key in Exchange]?: number } = {} let retries = 0 let bufferedAmount = 0 const messagesIterables = options.map((option) => { // let's map from provided options to options and normalizers that needs to be added for dataTypes provided in options const messages = streamNormalized( { ...option, withDisconnectMessages: true, onError: (error) => { const exchange = option.exchange as Exchange if (subSequentErrorsCount[exchange] === undefined) { subSequentErrorsCount[exchange] = 0 } subSequentErrorsCount[exchange]!++ if (option.withErrorMessages && !ws.closed) { ws.send( JSON.stringify({ type: 'error', exchange, localTimestamp: new Date(), details: error.message, subSequentErrorsCount: subSequentErrorsCount[exchange] }) ) } debug('WebSocket /ws-stream-normalized %s WS connection error: %o', exchange, error) } }, ...getNormalizers(option.dataTypes) ) // separately check if any computables are needed for given dataTypes const computables = getComputables(option.dataTypes) if (computables.length > 0) { return compute(messages, ...computables) } return messages }) const filterByDataType = constructDataTypeFilter(options) messages = messagesIterables.length === 1 ? messagesIterables[0] : combine(...messagesIterables) for await (const message of messages) { if (ws.closed) { return } const exchange = message.exchange as Exchange if (subSequentErrorsCount[exchange] !== undefined && subSequentErrorsCount[exchange]! >= 50) { ws.end(1011, `Too many subsequent errors when connecting to ${exchange} WS API`) return } if (!filterByDataType(message)) { continue } retries = 0 bufferedAmount = 0 // handle backpressure in case of slow clients while ((bufferedAmount = ws.getBufferedAmount()) > 0) { retries += 1 const isState = new Date().valueOf() - message.localTimestamp.valueOf() >= 6 // log stale messages, stale meaning message was not sent in 6 ms or more (2 retries) if (isState) { debug('Slow client, waiting %d ms, buffered amount: %d', 3 * retries, bufferedAmount) } if (retries > 300) { ws.end(1008, 'Too much backpressure') return } await wait(3 * retries) } ws.send(JSON.stringify(message)) if (message.type !== 'disconnect') { subSequentErrorsCount[exchange] = 0 } } while (ws.getBufferedAmount() > 0) { await wait(100) } ws.end(1000, 'WS stream-normalized finished') const endTimestamp = new Date().getTime() debug( 'WebSocket /ws-stream-normalized finished, options: %o, time: %d seconds', streamNormalizedOptions, (endTimestamp - startTimestamp) / 1000 ) } catch (e: any) { if (!ws.closed) { ws.end(1011, e.toString()) } debug('WebSocket /ws-stream-normalized error: %o', e) console.error('WebSocket /ws-stream-normalized error:', e) } finally { // this will close underlying open WS connections if (messages !== undefined) { messages!.return!() } } }