import { HubConnectionState } from "@microsoft/signalr"; import { groupBy, map, sumBy } from "lodash-es"; import moment from "moment-jalaali"; import { createContext, memo, ReactNode, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { OrderBookResponseVM, useGetExchangeV1PublicDepth, } from "../../services"; import { selectedSymbolStore } from "../../store"; import { applyDepthMock } from "../../utils"; import { useMarketTicker } from "../marketTicker"; import { SocketConnection } from "../socketConnection"; import { useDebounceAnimationFrameCallback } from "../useDebounceAnimationFrameCallback"; if (__MOCK__) { applyDepthMock(); } type BitAskState = { bids: [number, number][]; asks: [number, number][]; isLoading?: boolean; }; const OrderBookContext = createContext({ bids: [], asks: [], isLoading: false, }); type DepthItem = { e: string; // Event type E: number; // Event time s: string; // Symbol U: number; // First update ID in event u: number; // Final update ID in event b: [string, string][]; // Bids to be updated a: [string, string][]; // Asks to be updated }; interface Props { children?: ReactNode; } const OrderBookProvider = memo(({ children }) => { const ordersRef = useRef({ bids: [], asks: [] }); const isDataFetched = useRef(false); const ordersBufferRef = useRef([]); const { selectedSymbol } = selectedSymbolStore.useState(); const { getSymbolMarketTicker, isMarketTickerLoading } = useMarketTicker(); const marketTicker = getSymbolMarketTicker(selectedSymbol?.symbol); useEffect(() => { isDataFetched.current = false; ordersRef.current = { bids: [], asks: [] }; ordersBufferRef.current = []; }, [selectedSymbol]); const { refetch, isInitialLoading: isDepthLoading } = useGetExchangeV1PublicDepth( { symbol: selectedSymbol?.symbol, limit: 1000 }, { enabled: __MOCK__ || false, cacheTime: 0, staleTime: 0, onSuccess: (data) => { isDataFetched.current = true; ordersBufferRef.current = ordersBufferRef.current.filter( (item) => !(item.lastUpdateId! <= data.lastUpdateId!), ); const asks = (data?.asks || []) as [number, number][]; const bids = (data?.bids || []) as [number, number][]; ordersRef.current = { bids: bids, asks: asks, }; if (ordersBufferRef.current.length > 0) { ordersBufferRef.current.forEach((_data) => { handleDepthChange(_data); }); } else { handleDepthChange(); } }, }, ); const [state, setState] = useState({ bids: [], asks: [] }); const throttledSetOrders = useDebounceAnimationFrameCallback(setState, []); const handleDepthChange = (data?: OrderBookResponseVM) => { const _bids = (data?.bids as [number, number][]) || []; const _asks = (data?.asks as [number, number][]) || []; const { bids, asks } = ordersRef.current; const bidsMap = mergeOrders(bids, _bids); const asksMap = mergeOrders(asks, _asks); ordersRef.current = { bids: bidsMap, asks: asksMap, }; throttledSetOrders(ordersRef.current); }; SocketConnection.useEvent( `${selectedSymbol?.symbol.toLowerCase()}@depth`, (data: DepthItem) => { const { E, s, U, u, b, a } = data; if (s.toLowerCase() !== selectedSymbol?.symbol.toLowerCase()) { return; } const humanizedData: OrderBookResponseVM = { lastUpdateId: u, bids: b.map(([price, quantity]) => [Number(price), Number(quantity)]), asks: a.map(([price, quantity]) => [Number(price), Number(quantity)]), lastUpdateIdStream: u, transactionTime: moment(E).local().toDate().getTime(), firstUpdateId: U, symbol: s, }; if (isDataFetched.current) { handleDepthChange(humanizedData); return; } ordersBufferRef.current.push(humanizedData); }, { enabled: !!selectedSymbol?.symbol, }, ); useEffect(() => { if (!marketTicker) { return; } const interval = setInterval(() => { if ( SocketConnection.SocketConnection?.connection?.state === HubConnectionState.Connected ) { clearInterval(interval); refetch(); } }, 500); ordersBufferRef.current = []; isDataFetched.current = false; return () => { clearInterval(interval); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ selectedSymbol?.symbol, marketTicker?.smartTradeEngine, marketTicker?.lastMarketInfoChangeDate, ]); const isLoading = isMarketTickerLoading || isDepthLoading; const value = useMemo(() => ({ ...state, isLoading }), [isLoading, state]); return ( {children} ); }); const mergeOrders = ( oldList: [number, number][], newList: [number, number][], ) => { const oldGroup = groupBy(oldList, ([price]) => price); const newGroup = groupBy(newList, ([price]) => price); const group = map({ ...oldGroup, ...newGroup }, (o, idxPrice) => { return [Number(idxPrice), sumBy(o, ([, amount]) => amount)]; }) as [number, number][]; return group.filter(([, amount]) => amount !== 0).sort(([a], [b]) => b - a); }; const useOrdersBook = () => { const state = useContext(OrderBookContext); return state; }; export { OrderBookProvider, useOrdersBook };