diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts b/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts index 8d24b976f..5e3a4ad73 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts +++ b/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts @@ -1,3 +1,4 @@ +import produce from 'immer'; import { gql } from '@apollo/client'; import { makeDataProvider } from '@vegaprotocol/react-helpers'; import type { @@ -87,15 +88,16 @@ export const FILTERS_QUERY = gql` `; const update = ( - draft: SimpleMarkets_markets[], + data: SimpleMarkets_markets[], delta: SimpleMarketDataSub_marketData -) => { - const index = draft.findIndex((m) => m.id === delta.market.id); - if (index !== -1) { - draft[index].data = delta; - } - // @TODO - else push new market to draft -}; +) => + produce(data, (draft) => { + const index = draft.findIndex((m) => m.id === delta.market.id); + if (index !== -1) { + draft[index].data = delta; + } + // @TODO - else push new market to draft + }); const getData = (responseData: SimpleMarkets) => responseData.markets; const getDelta = ( diff --git a/libs/accounts/src/lib/accounts-data-provider.ts b/libs/accounts/src/lib/accounts-data-provider.ts index a330d4c9f..9bf905152 100644 --- a/libs/accounts/src/lib/accounts-data-provider.ts +++ b/libs/accounts/src/lib/accounts-data-provider.ts @@ -1,3 +1,4 @@ +import produce from 'immer'; import { gql } from '@apollo/client'; import type { Accounts, @@ -52,17 +53,18 @@ export const getId = ( ) => `${data.type}-${data.asset.symbol}-${data.market?.id ?? 'null'}`; const update = ( - draft: Accounts_party_accounts[], + data: Accounts_party_accounts[], delta: AccountSubscribe_accounts -) => { - const id = getId(delta); - const index = draft.findIndex((a) => getId(a) === id); - if (index !== -1) { - draft[index] = delta; - } else { - draft.push(delta); - } -}; +) => + produce(data, (draft) => { + const id = getId(delta); + const index = draft.findIndex((a) => getId(a) === id); + if (index !== -1) { + draft[index] = delta; + } else { + draft.push(delta); + } + }); const getData = (responseData: Accounts): Accounts_party_accounts[] | null => responseData.party ? responseData.party.accounts : null; const getDelta = ( diff --git a/libs/market-depth/src/lib/depth-chart.tsx b/libs/market-depth/src/lib/depth-chart.tsx index 076f8e79f..4285498cb 100644 --- a/libs/market-depth/src/lib/depth-chart.tsx +++ b/libs/market-depth/src/lib/depth-chart.tsx @@ -1,5 +1,4 @@ import { DepthChart } from 'pennant'; -import { produce } from 'immer'; import throttle from 'lodash/throttle'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { @@ -92,28 +91,31 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => { if (!dataRef.current) { return false; } - dataRef.current = produce(dataRef.current, (draft) => { - if (delta.buy) { - draft.data.buy = updateLevels( - draft.data.buy, - delta.buy, - decimalPlacesRef.current - ); - } - if (delta.sell) { - draft.data.sell = updateLevels( - draft.data.sell, - delta.sell, - decimalPlacesRef.current - ); - } - draft.midPrice = delta.market.data?.staticMidPrice + dataRef.current = { + ...dataRef.current, + midPrice: delta.market.data?.staticMidPrice ? formatMidPrice( delta.market.data?.staticMidPrice, decimalPlacesRef.current ) - : undefined; - }); + : undefined, + data: { + buy: delta.buy + ? updateLevels( + dataRef.current.data.buy, + delta.buy, + decimalPlacesRef.current + ) + : dataRef.current.data.buy, + sell: delta.sell + ? updateLevels( + dataRef.current.data.sell, + delta.sell, + decimalPlacesRef.current + ) + : dataRef.current.data.sell, + }, + }; setDepthDataThrottledRef.current(dataRef.current); return true; }, diff --git a/libs/market-depth/src/lib/market-depth-data-provider.ts b/libs/market-depth/src/lib/market-depth-data-provider.ts index 3ce75aa92..f82e70fe9 100644 --- a/libs/market-depth/src/lib/market-depth-data-provider.ts +++ b/libs/market-depth/src/lib/market-depth-data-provider.ts @@ -86,27 +86,31 @@ const sequenceNumbers: Record = {}; const update: Update< MarketDepth_market, MarketDepthSubscription_marketDepthUpdate -> = (draft, delta, reload) => { - if (delta.market.id !== draft.id) { - return; +> = (data, delta, reload) => { + if (delta.market.id !== data.id) { + return data; } const sequenceNumber = Number(delta.sequenceNumber); if (sequenceNumber <= sequenceNumbers[delta.market.id]) { - return; + return data; } + /* if (sequenceNumber - 1 !== sequenceNumbers[delta.market.id]) { sequenceNumbers[delta.market.id] = 0; reload(); return; } + */ sequenceNumbers[delta.market.id] = sequenceNumber; - Object.assign(draft.data, delta.market.data); + const updatedData = { ...data }; + data.data = delta.market.data; if (delta.buy) { - draft.depth.buy = updateLevels(draft.depth.buy ?? [], delta.buy); + updatedData.depth.buy = updateLevels(data.depth.buy ?? [], delta.buy); } if (delta.sell) { - draft.depth.sell = updateLevels(draft.depth.sell ?? [], delta.sell); + updatedData.depth.sell = updateLevels(data.depth.sell ?? [], delta.sell); } + return updatedData; }; const getData = (responseData: MarketDepth) => { diff --git a/libs/market-depth/src/lib/orderbook-data.spec.ts b/libs/market-depth/src/lib/orderbook-data.spec.ts index a94544cbc..e69e299b2 100644 --- a/libs/market-depth/src/lib/orderbook-data.spec.ts +++ b/libs/market-depth/src/lib/orderbook-data.spec.ts @@ -55,10 +55,8 @@ describe('compactRows', () => { '1097': 3, '1098': 2, '1099': 1, - '1100': 0, }); expect(orderbookRows[orderbookRows.length - 1].bidByLevel).toEqual({ - '901': 0, '902': 1, '903': 2, '904': 3, @@ -81,7 +79,7 @@ describe('compactRows', () => { }); describe('updateLevels', () => { - const levels: MarketDepth_market_depth_sell[] = new Array(10) + let levels: MarketDepth_market_depth_sell[] = new Array(10) .fill(null) .map((n, i) => ({ __typename: 'PriceLevel', @@ -96,9 +94,9 @@ describe('updateLevels', () => { volume: '0', numberOfOrders: '0', }; - updateLevels(levels, [removeFirstRow]); + levels = updateLevels(levels, [removeFirstRow]); expect(levels[0].price).toEqual('20'); - updateLevels(levels, [removeFirstRow]); + levels = updateLevels(levels, [removeFirstRow]); expect(levels[0].price).toEqual('20'); expect(updateLevels([], [removeFirstRow])).toEqual([]); const addFirstRow: MarketDepthSubscription_marketDepthUpdate_sell = { @@ -107,7 +105,7 @@ describe('updateLevels', () => { volume: '10', numberOfOrders: '10', }; - updateLevels(levels, [addFirstRow]); + levels = updateLevels(levels, [addFirstRow]); expect(levels[0].price).toEqual('10'); const addBeforeLastRow: MarketDepthSubscription_marketDepthUpdate_sell = { __typename: 'PriceLevel', @@ -115,7 +113,7 @@ describe('updateLevels', () => { volume: '95', numberOfOrders: '95', }; - updateLevels(levels, [addBeforeLastRow]); + levels = updateLevels(levels, [addBeforeLastRow]); expect(levels[levels.length - 2].price).toEqual('95'); const addAtTheEnd: MarketDepthSubscription_marketDepthUpdate_sell = { __typename: 'PriceLevel', @@ -123,7 +121,7 @@ describe('updateLevels', () => { volume: '115', numberOfOrders: '115', }; - updateLevels(levels, [addAtTheEnd]); + levels = updateLevels(levels, [addAtTheEnd]); expect(levels[levels.length - 1].price).toEqual('115'); const updateLastRow: MarketDepthSubscription_marketDepthUpdate_sell = { __typename: 'PriceLevel', @@ -131,7 +129,7 @@ describe('updateLevels', () => { volume: '116', numberOfOrders: '115', }; - updateLevels(levels, [updateLastRow]); + levels = updateLevels(levels, [updateLastRow]); expect(levels[levels.length - 1]).toEqual(updateLastRow); expect(updateLevels([], [updateLastRow])).toEqual([updateLastRow]); }); diff --git a/libs/market-depth/src/lib/orderbook-data.ts b/libs/market-depth/src/lib/orderbook-data.ts index 9ff332d97..f39604669 100644 --- a/libs/market-depth/src/lib/orderbook-data.ts +++ b/libs/market-depth/src/lib/orderbook-data.ts @@ -1,4 +1,3 @@ -import produce from 'immer'; import groupBy from 'lodash/groupBy'; import { VolumeType } from '@vegaprotocol/react-helpers'; import { MarketTradingMode } from '@vegaprotocol/types'; @@ -31,6 +30,8 @@ export interface OrderbookRowData { cumulativeVol: CumulativeVol; } +type PartialOrderbookRowData = Pick; + export type OrderbookData = Partial< Omit > & { rows: OrderbookRowData[] | null }; @@ -75,21 +76,31 @@ const updateRelativeData = (data: OrderbookRowData[]) => { }); }; +export const createPartialRow = ( + price: string, + volume = 0, + dataType?: VolumeType +): PartialOrderbookRowData => ({ + price, + ask: dataType === VolumeType.ask ? volume : 0, + bid: dataType === VolumeType.bid ? volume : 0, +}); + +export const extendRow = (row: PartialOrderbookRowData): OrderbookRowData => + Object.assign(row, { + cumulativeVol: { + ask: 0, + bid: 0, + }, + askByLevel: row.ask ? { [row.price]: row.ask } : {}, + bidByLevel: row.bid ? { [row.price]: row.bid } : {}, + }); + export const createRow = ( price: string, volume = 0, dataType?: VolumeType -): OrderbookRowData => ({ - price, - ask: dataType === VolumeType.ask ? volume : 0, - bid: dataType === VolumeType.bid ? volume : 0, - cumulativeVol: { - ask: dataType === VolumeType.ask ? volume : 0, - bid: dataType === VolumeType.bid ? volume : 0, - }, - askByLevel: dataType === VolumeType.ask ? { [price]: volume } : {}, - bidByLevel: dataType === VolumeType.bid ? { [price]: volume } : {}, -}); +): OrderbookRowData => extendRow(createPartialRow(price, volume, dataType)); const mapRawData = (dataType: VolumeType.ask | VolumeType.bid) => @@ -99,8 +110,8 @@ const mapRawData = | MarketDepthSubscription_marketDepthUpdate_sell | MarketDepth_market_depth_buy | MarketDepthSubscription_marketDepthUpdate_buy - ): OrderbookRowData => - createRow(data.price, Number(data.volume), dataType); + ): PartialOrderbookRowData => + createPartialRow(data.price, Number(data.volume), dataType); /** * @summary merges sell amd buy data, orders by price desc, group by price level, counts cumulative and relative values @@ -121,37 +132,38 @@ export const compactRows = ( resolution: number ) => { // map raw sell data to OrderbookData - const askOrderbookData = [...(sell ?? [])].map( + const askOrderbookData = [...(sell ?? [])].map( mapRawData(VolumeType.ask) ); // map raw buy data to OrderbookData - const bidOrderbookData = [...(buy ?? [])].map( + const bidOrderbookData = [...(buy ?? [])].map( mapRawData(VolumeType.bid) ); - // group by price level - const groupedByLevel = groupBy( + const groupedByLevel = groupBy( [...askOrderbookData, ...bidOrderbookData], (row) => getPriceLevel(row.price, resolution) ); - - // create single OrderbookData from grouped OrderbookData[], sum volumes and atore volume by level - const orderbookData = Object.keys(groupedByLevel).reduce( - (rows, price) => - rows.concat( - groupedByLevel[price].reduce( - (a, c) => ({ - ...a, - ask: a.ask + c.ask, - askByLevel: Object.assign(a.askByLevel, c.askByLevel), - bid: (a.bid ?? 0) + (c.bid ?? 0), - bidByLevel: Object.assign(a.bidByLevel, c.bidByLevel), - }), - createRow(price) - ) - ), - [] - ); + const orderbookData: OrderbookRowData[] = []; + Object.keys(groupedByLevel).forEach((price) => { + const row = extendRow( + groupedByLevel[price].pop() as PartialOrderbookRowData + ); + row.price = price; + let subRow: PartialOrderbookRowData | undefined; + // eslint-disable-next-line no-cond-assign + while ((subRow = groupedByLevel[price].pop())) { + row.ask += subRow.ask; + row.bid += subRow.bid; + if (subRow.ask) { + row.askByLevel[subRow.price] = subRow.ask; + } + if (subRow.bid) { + row.bidByLevel[subRow.price] = subRow.bid; + } + } + orderbookData.push(row); + }); // order by price, it's safe to cast to number price diff should not exceed Number.MAX_SAFE_INTEGER orderbookData.sort((a, b) => Number(BigInt(b.price) - BigInt(a.price))); // count cumulative volumes @@ -163,11 +175,9 @@ export const compactRows = ( (i !== 0 ? orderbookData[i - 1].cumulativeVol.bid : 0); } for (let i = maxIndex; i >= 0; i--) { - if (!orderbookData[i].cumulativeVol.ask) { - orderbookData[i].cumulativeVol.ask = - orderbookData[i].ask + - (i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0); - } + orderbookData[i].cumulativeVol.ask = + orderbookData[i].ask + + (i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0); } } // count relative volumes @@ -186,13 +196,13 @@ export const compactRows = ( */ const partiallyUpdateCompactedRows = ( dataType: VolumeType, - draft: OrderbookRowData[], + data: OrderbookRowData[], delta: | MarketDepthSubscription_marketDepthUpdate_sell | MarketDepthSubscription_marketDepthUpdate_buy, resolution: number, modifiedIndex: number -) => { +): [number, OrderbookRowData[]] => { const { price } = delta; const volume = Number(delta.volume); const priceLevel = getPriceLevel(price, resolution); @@ -201,28 +211,36 @@ const partiallyUpdateCompactedRows = ( const oppositeVolKey = isAskDataType ? 'bid' : 'ask'; const volByLevelKey = isAskDataType ? 'askByLevel' : 'bidByLevel'; const resolveModifiedIndex = isAskDataType ? Math.max : Math.min; - let index = draft.findIndex((data) => data.price === priceLevel); + let index = data.findIndex((row) => row.price === priceLevel); if (index !== -1) { modifiedIndex = resolveModifiedIndex(modifiedIndex, index); - draft[index][volKey] = - draft[index][volKey] - (draft[index][volByLevelKey][price] || 0) + volume; - draft[index][volByLevelKey][price] = volume; + data[index] = { + ...data[index], + [volKey]: + data[index][volKey] - (data[index][volByLevelKey][price] || 0) + volume, + [volByLevelKey]: { + ...data[index][volByLevelKey], + [price]: volume, + }, + }; + return [modifiedIndex, [...data]]; } else { const newData: OrderbookRowData = createRow(priceLevel, volume, dataType); - index = draft.findIndex((data) => BigInt(data.price) < BigInt(priceLevel)); + index = data.findIndex((row) => BigInt(row.price) < BigInt(priceLevel)); if (index !== -1) { - draft.splice(index, 0, newData); newData.cumulativeVol[oppositeVolKey] = - draft[index + (isAskDataType ? -1 : 1)]?.cumulativeVol[ - oppositeVolKey - ] ?? 0; + data[index + (isAskDataType ? 0 : 1)]?.cumulativeVol[oppositeVolKey] ?? + 0; modifiedIndex = resolveModifiedIndex(modifiedIndex, index); + return [ + modifiedIndex, + [...data.slice(0, index), newData, ...data.slice(index)], + ]; } else { - draft.push(newData); - modifiedIndex = draft.length - 1; + modifiedIndex = data.length - 1; + return [modifiedIndex, [...data, newData]]; } } - return modifiedIndex; }; /** @@ -239,59 +257,66 @@ export const updateCompactedRows = ( sell: MarketDepthSubscription_marketDepthUpdate_sell[] | null, buy: MarketDepthSubscription_marketDepthUpdate_buy[] | null, resolution: number -) => - produce(rows, (draft) => { - let sellModifiedIndex = -1; - sell?.forEach((delta) => { - sellModifiedIndex = partiallyUpdateCompactedRows( - VolumeType.ask, - draft, - delta, - resolution, - sellModifiedIndex - ); - }); - let buyModifiedIndex = draft.length; - buy?.forEach((delta) => { - buyModifiedIndex = partiallyUpdateCompactedRows( - VolumeType.bid, - draft, - delta, - resolution, - buyModifiedIndex - ); - }); - - // update cummulative ask only below hihgest modified price level - if (sellModifiedIndex !== -1) { - for (let i = Math.min(sellModifiedIndex, draft.length - 2); i >= 0; i--) { - draft[i].cumulativeVol.ask = - draft[i + 1].cumulativeVol.ask + draft[i].ask; - } - } - // update cummulative bid only above lowest modified price level - if (buyModifiedIndex !== draft.length) { - for ( - let i = Math.max(buyModifiedIndex, 1), l = draft.length; - i < l; - i++ - ) { - draft[i].cumulativeVol.bid = - draft[i - 1].cumulativeVol.bid + draft[i].bid; - } - } - let index = 0; - // remove levels that do not have any volume - while (index < draft.length) { - if (!draft[index].ask && !draft[index].bid) { - draft.splice(index, 1); - } else { - index += 1; - } - } - // count relative volumes - updateRelativeData(draft); +) => { + let sellModifiedIndex = -1; + let data = [...rows]; + sell?.forEach((delta) => { + [sellModifiedIndex, data] = partiallyUpdateCompactedRows( + VolumeType.ask, + data, + delta, + resolution, + sellModifiedIndex + ); }); + let buyModifiedIndex = data.length; + buy?.forEach((delta) => { + [buyModifiedIndex, data] = partiallyUpdateCompactedRows( + VolumeType.bid, + data, + delta, + resolution, + buyModifiedIndex + ); + }); + + // update cummulative ask only below hihgest modified price level + if (sellModifiedIndex !== -1) { + for (let i = Math.min(sellModifiedIndex, data.length - 2); i >= 0; i--) { + data[i] = { + ...data[i], + cumulativeVol: { + ...data[i].cumulativeVol, + ask: data[i + 1].cumulativeVol.ask + data[i].ask, + }, + }; + } + } + // update cummulative bid only above lowest modified price level + if (buyModifiedIndex !== data.length) { + for (let i = Math.max(buyModifiedIndex, 1), l = data.length; i < l; i++) { + data[i] = { + ...data[i], + cumulativeVol: { + ...data[i].cumulativeVol, + bid: data[i - 1].cumulativeVol.bid + data[i].bid, + }, + }; + } + } + let index = 0; + // remove levels that do not have any volume + while (index < data.length) { + if (!data[index].ask && !data[index].bid) { + data.splice(index, 1); + } else { + index += 1; + } + } + // count relative volumes + updateRelativeData(data); + return data; +}; export const mapMarketData = ( data: @@ -319,23 +344,24 @@ export const mapMarketData = ( * @returns */ export const updateLevels = ( - levels: (MarketDepth_market_depth_buy | MarketDepth_market_depth_sell)[], + draft: (MarketDepth_market_depth_buy | MarketDepth_market_depth_sell)[], updates: ( | MarketDepthSubscription_marketDepthUpdate_buy | MarketDepthSubscription_marketDepthUpdate_sell )[] ) => { + const levels = [...draft]; updates.forEach((update) => { let index = levels.findIndex((level) => level.price === update.price); if (index !== -1) { if (update.volume === '0') { levels.splice(index, 1); } else { - Object.assign(levels[index], update); + levels[index] = update; } } else if (update.volume !== '0') { index = levels.findIndex( - (level) => Number(level.price) > Number(update.price) + (level) => BigInt(level.price) > BigInt(update.price) ); if (index !== -1) { levels.splice(index, 0, update); diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index c5a518534..af814d05d 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -1,5 +1,4 @@ import throttle from 'lodash/throttle'; -import produce from 'immer'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { Orderbook } from './orderbook'; import { useDataProvider } from '@vegaprotocol/react-helpers'; @@ -25,27 +24,52 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { rows: null, }); const dataRef = useRef({ rows: null }); - const setOrderbookDataThrottled = useRef(throttle(setOrderbookData, 1000)); + const deltaRef = useRef(); + const updateOrderbookData = useRef( + throttle(() => { + if (!deltaRef.current) { + return; + } + dataRef.current = { + ...deltaRef.current.market.data, + ...mapMarketData(deltaRef.current.market.data, resolutionRef.current), + rows: updateCompactedRows( + dataRef.current.rows ?? [], + deltaRef.current.sell, + deltaRef.current.buy, + resolutionRef.current + ), + }; + deltaRef.current = undefined; + setOrderbookData(dataRef.current); + }, 1000) + ); const update = useCallback( (delta: MarketDepthSubscription_marketDepthUpdate) => { if (!dataRef.current.rows) { return false; } - dataRef.current = produce(dataRef.current, (draft) => { - Object.assign(draft, delta.market.data); - draft.rows = updateCompactedRows( - draft.rows ?? [], - delta.sell, - delta.buy, - resolutionRef.current - ); - Object.assign( - draft, - mapMarketData(delta.market.data, resolutionRef.current) - ); - }); - setOrderbookDataThrottled.current(dataRef.current); + if (deltaRef.current) { + deltaRef.current.market = delta.market; + if (delta.sell) { + if (deltaRef.current.sell) { + deltaRef.current.sell.push(...delta.sell); + } else { + deltaRef.current.sell = delta.sell; + } + } + if (delta.buy) { + if (deltaRef.current.buy) { + deltaRef.current.buy.push(...delta.buy); + } else { + deltaRef.current.buy = delta.buy; + } + } + } else { + deltaRef.current = delta; + } + updateOrderbookData.current(); return true; }, // using resolutionRef.current to avoid using resolution as a dependency - it will cause data provider restart on resolution change diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index 8feede4e8..37df5c8d6 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -176,7 +176,10 @@ export const Orderbook = ({ // adjust to current rows position scrollTop += (scrollTopRef.current % rowHeight) - (scrollTop % rowHeight); - const priceCenterScrollOffset = Math.max(0, Math.min(scrollTop)); + const priceCenterScrollOffset = Math.max( + 0, + Math.min(scrollTop, numberOfRows * rowHeight - viewportHeight) + ); if (scrollTopRef.current !== priceCenterScrollOffset) { updateScrollOffset(priceCenterScrollOffset); scrollTopRef.current = priceCenterScrollOffset; @@ -184,7 +187,13 @@ export const Orderbook = ({ } } }, - [maxPriceLevel, resolution, viewportHeight, updateScrollOffset] + [ + maxPriceLevel, + resolution, + viewportHeight, + numberOfRows, + updateScrollOffset, + ] ); useEffect(() => { @@ -199,23 +208,36 @@ export const Orderbook = ({ return; } priceInCenter.current = undefined; - setLockOnMidPrice(true); - scrollToPrice( - getPriceLevel( - BigInt(bestStaticOfferPrice) + - (BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) / - BigInt(2), - resolution - ) + let midPrice = getPriceLevel( + BigInt(bestStaticOfferPrice) + + (BigInt(bestStaticBidPrice) - BigInt(bestStaticOfferPrice)) / BigInt(2), + resolution ); - }, [bestStaticOfferPrice, bestStaticBidPrice, scrollToPrice, resolution]); + if (BigInt(midPrice) > BigInt(maxPriceLevel)) { + midPrice = maxPriceLevel; + } else { + const minPriceLevel = + BigInt(maxPriceLevel) - BigInt(Math.floor(numberOfRows * resolution)); + if (BigInt(midPrice) < minPriceLevel) { + midPrice = minPriceLevel.toString(); + } + } + scrollToPrice(midPrice); + setLockOnMidPrice(true); + }, [ + bestStaticOfferPrice, + bestStaticBidPrice, + scrollToPrice, + resolution, + maxPriceLevel, + numberOfRows, + ]); // adjust scroll position to keep selected price in center useLayoutEffect(() => { if (resolutionRef.current !== resolution) { priceInCenter.current = undefined; resolutionRef.current = resolution; - setLockOnMidPrice(true); } if (priceInCenter.current) { scrollToPrice(priceInCenter.current); @@ -238,21 +260,19 @@ export const Orderbook = ({ return () => window.removeEventListener('resize', handleResize); }, []); - const renderedRows = useMemo(() => { - let offset = Math.max(0, Math.round(scrollOffset / rowHeight)); - const prependingBufferSize = Math.min(bufferSize, offset); - offset -= prependingBufferSize; - const viewportSize = Math.round(viewportHeight / rowHeight); - const limit = Math.min( - prependingBufferSize + viewportSize + bufferSize, - numberOfRows - offset - ); - return { - offset, - limit, - data: getRowsToRender(rows, resolution, offset, limit), - }; - }, [rows, scrollOffset, resolution, viewportHeight, numberOfRows]); + let offset = Math.max(0, Math.round(scrollOffset / rowHeight)); + const prependingBufferSize = Math.min(bufferSize, offset); + offset -= prependingBufferSize; + const viewportSize = Math.round(viewportHeight / rowHeight); + const limit = Math.min( + prependingBufferSize + viewportSize + bufferSize, + numberOfRows - offset + ); + const renderedRows = { + offset, + limit, + data: getRowsToRender(rows, resolution, offset, limit), + }; const paddingTop = renderedRows.offset * rowHeight; const paddingBottom = diff --git a/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts b/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts index efed373a6..79d612e52 100644 --- a/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts +++ b/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts @@ -1,3 +1,4 @@ +import produce from 'immer'; import { gql } from '@apollo/client'; import type { Markets, @@ -90,13 +91,14 @@ const MARKET_DATA_SUB = gql` } `; -const update = (draft: Markets_markets[], delta: MarketDataSub_marketData) => { - const index = draft.findIndex((m) => m.id === delta.market.id); - if (index !== -1) { - draft[index].data = delta; - } - // @TODO - else push new market to draft -}; +const update = (data: Markets_markets[], delta: MarketDataSub_marketData) => + produce(data, (draft) => { + const index = draft.findIndex((m) => m.id === delta.market.id); + if (index !== -1) { + draft[index].data = delta; + } + // @TODO - else push new market to draft + }); const getData = (responseData: Markets): Markets_markets[] | null => responseData.markets; const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData => diff --git a/libs/order-list/src/lib/orders-data-provider.ts b/libs/order-list/src/lib/orders-data-provider.ts index adc6cb2f7..ccd6d88fb 100644 --- a/libs/order-list/src/lib/orders-data-provider.ts +++ b/libs/order-list/src/lib/orders-data-provider.ts @@ -1,3 +1,4 @@ +import produce from 'immer'; import { gql } from '@apollo/client'; import { makeDataProvider } from '@vegaprotocol/react-helpers'; import type { OrderFields } from './__generated__/OrderFields'; @@ -78,19 +79,20 @@ export const prepareIncomingOrders = (delta: OrderFields[]) => { return incoming; }; -const update = (draft: OrderFields[], delta: OrderFields[]) => { - const incoming = prepareIncomingOrders(delta); +const update = (data: OrderFields[], delta: OrderFields[]) => + produce(data, (draft) => { + const incoming = prepareIncomingOrders(delta); - // Add or update incoming orders - incoming.forEach((order) => { - const index = draft.findIndex((o) => o.id === order.id); - if (index === -1) { - draft.unshift(order); - } else { - draft[index] = order; - } + // Add or update incoming orders + incoming.forEach((order) => { + const index = draft.findIndex((o) => o.id === order.id); + if (index === -1) { + draft.unshift(order); + } else { + draft[index] = order; + } + }); }); -}; const getData = (responseData: Orders): Orders_party_orders[] | null => responseData?.party?.orders || null; diff --git a/libs/positions/src/lib/positions-data-provider.ts b/libs/positions/src/lib/positions-data-provider.ts index 9776f201f..e030eb437 100644 --- a/libs/positions/src/lib/positions-data-provider.ts +++ b/libs/positions/src/lib/positions-data-provider.ts @@ -1,3 +1,4 @@ +import produce from 'immer'; import { gql } from '@apollo/client'; import type { Positions, @@ -75,16 +76,17 @@ export const POSITIONS_SUB = gql` `; const update = ( - draft: Positions_party_positions[], + data: Positions_party_positions[], delta: PositionSubscribe_positions -) => { - const index = draft.findIndex((m) => m.market.id === delta.market.id); - if (index !== -1) { - draft[index] = delta; - } else { - draft.push(delta); - } -}; +) => + produce(data, (draft) => { + const index = draft.findIndex((m) => m.market.id === delta.market.id); + if (index !== -1) { + draft[index] = delta; + } else { + draft.push(delta); + } + }); const getData = (responseData: Positions): Positions_party_positions[] | null => responseData.party ? responseData.party.positions : null; const getDelta = ( diff --git a/libs/react-helpers/src/lib/generic-data-provider.ts b/libs/react-helpers/src/lib/generic-data-provider.ts index 101fc9a04..41113c61b 100644 --- a/libs/react-helpers/src/lib/generic-data-provider.ts +++ b/libs/react-helpers/src/lib/generic-data-provider.ts @@ -1,5 +1,3 @@ -import { produce } from 'immer'; -import type { Draft } from 'immer'; import type { ApolloClient, DocumentNode, @@ -34,11 +32,7 @@ export interface Subscribe { type Query = DocumentNode | TypedDocumentNode; export interface Update { - ( - draft: Draft, - delta: Delta, - reload: (forceReset?: boolean) => void - ): void; + (data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data; } interface GetData { @@ -105,14 +99,12 @@ function makeDataProviderInternal( data = getData(res.data); // if there was some updates received from subscription during initial query loading apply them on just received data if (data && updateQueue && updateQueue.length > 0) { - data = produce(data, (draft) => { - while (updateQueue.length) { - const delta = updateQueue.shift(); - if (delta) { - update(draft, delta, reload); - } + while (updateQueue.length) { + const delta = updateQueue.shift(); + if (delta) { + data = update(data, delta, reload); } - }); + } } } catch (e) { // if error will occur data provider stops subscription @@ -168,9 +160,7 @@ function makeDataProviderInternal( if (loading || !data) { updateQueue.push(delta); } else { - const newData = produce(data, (draft) => { - update(draft, delta, reload); - }); + const newData = update(data, delta, reload); if (newData === data) { return; } diff --git a/libs/trades/src/lib/trades-data-provider.ts b/libs/trades/src/lib/trades-data-provider.ts index 6381cb8b0..2234e530e 100644 --- a/libs/trades/src/lib/trades-data-provider.ts +++ b/libs/trades/src/lib/trades-data-provider.ts @@ -4,6 +4,7 @@ import type { TradeFields } from './__generated__/TradeFields'; import type { Trades } from './__generated__/Trades'; import type { TradesSub } from './__generated__/TradesSub'; import orderBy from 'lodash/orderBy'; +import produce from 'immer'; export const MAX_TRADES = 50; @@ -52,17 +53,18 @@ export const sortTrades = (trades: TradeFields[]) => { ); }; -const update = (draft: TradeFields[], delta: TradeFields[]) => { - const incoming = sortTrades(delta); +const update = (data: TradeFields[], delta: TradeFields[]) => + produce(data, (draft) => { + const incoming = sortTrades(delta); - // Add new trades to the top - draft.unshift(...incoming); + // Add new trades to the top + draft.unshift(...incoming); - // Remove old trades from the bottom - if (draft.length > MAX_TRADES) { - draft.splice(MAX_TRADES, draft.length - MAX_TRADES); - } -}; + // Remove old trades from the bottom + if (draft.length > MAX_TRADES) { + draft.splice(MAX_TRADES, draft.length - MAX_TRADES); + } + }); const getData = (responseData: Trades): TradeFields[] | null => responseData.market ? responseData.market.trades : null;