From 4698e532c1774c64432507915fe3afaf6b369e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Wed, 23 Mar 2022 13:47:45 +0100 Subject: [PATCH] Use data markets data provider instead of use-markets hook --- .../data-providers/markets-data-provider.ts | 166 ++++++++++++++++++ apps/trading/hooks/use-markets.ts | 16 +- apps/trading/pages/markets/index.page.tsx | 82 +++++++-- .../market-list/src/lib/market-list-table.tsx | 11 +- .../ag-grid/ag-grid-dynamic-themed.tsx | 23 +-- .../components/ag-grid/ag-grid-dynamic.tsx | 8 +- package.json | 1 + yarn.lock | 12 ++ 8 files changed, 277 insertions(+), 42 deletions(-) create mode 100644 apps/trading/data-providers/markets-data-provider.ts diff --git a/apps/trading/data-providers/markets-data-provider.ts b/apps/trading/data-providers/markets-data-provider.ts new file mode 100644 index 000000000..28c97ff3b --- /dev/null +++ b/apps/trading/data-providers/markets-data-provider.ts @@ -0,0 +1,166 @@ +import { gql } from '@apollo/client'; +import { produce } from 'immer'; +import type { ApolloClient } from '@apollo/client'; +import type { Subscription } from 'zen-observable-ts'; +import { + Markets, + Markets_markets, + MarketDataSub, + MarketDataSub_marketData, +} from '@vegaprotocol/graphql'; + +const MARKET_DATA_FRAGMENT = gql` + fragment MarketDataFields on MarketData { + market { + id + state + tradingMode + } + bestBidPrice + bestOfferPrice + markPrice + } +`; + +const MARKETS_QUERY = gql` + ${MARKET_DATA_FRAGMENT} + query Markets { + markets { + id + name + decimalPlaces + data { + ...MarketDataFields + } + tradableInstrument { + instrument { + code + product { + ... on Future { + settlementAsset { + symbol + } + } + } + } + } + } + } +`; + +const MARKET_DATA_SUB = gql` + ${MARKET_DATA_FRAGMENT} + subscription MarketDataSub { + marketData { + ...MarketDataFields + } + } +`; + +export interface CallbackArg { + data?: Markets_markets[]; + error?: Error; + loading: boolean; + delta?: MarketDataSub_marketData; +} + +export interface Callback { + (arg: CallbackArg): void; +} + +const callbacks: Callback[] = []; +const updateQueue: MarketDataSub_marketData[] = []; + +let data: Markets_markets[] = undefined; +let error: Error = undefined; +let loading = false; +let client: ApolloClient = undefined; +let subscription: Subscription = undefined; + +const notify = (callback, delta?: MarketDataSub_marketData) => { + callback({ + data, + error, + loading, + delta, + }); +}; + +const notifyAll = (delta?: MarketDataSub_marketData) => { + callbacks.forEach((callback) => notify(callback, delta)); +}; + +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 initialize = async () => { + if (subscription) { + return; + } + loading = true; + error = null; + notifyAll(); + subscription = client + .subscribe({ + query: MARKET_DATA_SUB, + }) + .subscribe(({ data: delta }) => { + if (loading) { + updateQueue.push(delta.marketData); + } else { + data = produce(data, (draft) => { + update(draft, delta.marketData); + }); + notifyAll(delta.marketData); + } + }); + try { + const res = await client.query({ + query: MARKETS_QUERY, + }); + data = res.data.markets; + if (updateQueue) { + data = produce(data, (draft) => { + while (updateQueue.length) { + update(draft, updateQueue.shift()); + } + }); + } + } catch (e) { + error = e; + subscription.unsubscribe(); + subscription = undefined; + } finally { + loading = false; + notifyAll(); + } +}; + +const unsubscribe = (callback: Callback) => { + callbacks.splice(callbacks.indexOf(callback), 1); + if (callbacks.length === 0) { + subscription.unsubscribe(); + subscription = undefined; + data = undefined; + error = undefined; + loading = false; + } +}; + +export const subscribe = (c: ApolloClient, callback) => { + if (!client) { + client = c; + } + callbacks.push(callback); + if (callbacks.length === 1) { + initialize(); + } else { + notify(callback); + } + return () => unsubscribe(callback); +}; diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts index e31cb67f5..aa54751d0 100644 --- a/apps/trading/hooks/use-markets.ts +++ b/apps/trading/hooks/use-markets.ts @@ -81,21 +81,26 @@ export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => // Make initial fetch useEffect(() => { - (async () => { + const fetchOrders = async () => { setLoading(true); + try { const res = await client.query({ query: MARKETS_QUERY, }); + if (!res.data.markets?.length) return; + setMarkets(res.data.markets); } catch (err) { setError(err); } finally { setLoading(false); } - })(); - }, [client]); + }; + + fetchOrders(); + }, [mergeMarketData, client]); // Start subscription useEffect(() => { @@ -106,9 +111,6 @@ export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => query: MARKET_DATA_SUB, }) .subscribe(({ data }) => { - if (updateCallback) { - updateCallback(data.marketData); - } mergeMarketData(data.marketData); }); @@ -117,7 +119,7 @@ export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => sub.unsubscribe(); } }; - }, [client, mergeMarketData, updateCallback]); + }, [client, mergeMarketData]); return { markets, error, loading }; }; diff --git a/apps/trading/pages/markets/index.page.tsx b/apps/trading/pages/markets/index.page.tsx index dd7ad989f..704a78305 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -1,18 +1,75 @@ -import { Markets } from '@vegaprotocol/graphql'; +import { useState, useEffect, useRef } from 'react'; +import { produce } from 'immer'; +import assign from 'assign-deep'; +import { useApolloClient } from '@apollo/client'; import { useRouter } from 'next/router'; -import { MarketListTable } from '@vegaprotocol/market-list'; -import { useMarkets } from '../../hooks/use-markets'; import { AsyncRenderer } from '../../components/async-renderer'; -import { updateCallback } from '@vegaprotocol/react-helpers'; +import { MarketListTable, getRowNodeId } from '@vegaprotocol/market-list'; +import { + Markets_markets, + Markets_markets_data +} from '@vegaprotocol/graphql'; + +import { subscribe } from '../../data-providers/markets-data-provider'; +import type { CallbackArg } from '../../data-providers/markets-data-provider'; +import type { AgGridReact } from 'ag-grid-react'; const Markets = () => { const { pathname, push } = useRouter(); - const { markets, error, loading } = useMarkets(updateCallback); + const [markets, setMarkets] = useState(undefined); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(undefined); + const client = useApolloClient(); + const gridRef = useRef(); + const initialized = useRef(false); + + useEffect(() => { + return subscribe(client, ({ data, error, loading, delta }: CallbackArg) => { + setError(error); + setLoading(loading); + if (!error && !loading) { + if (!initialized.current || !gridRef.current) { + initialized.current = true; + setMarkets(data); + } else { + const update: Markets_markets[] = []; + const add: Markets_markets[] = []; + + // split into updates and adds + if (!gridRef.current) return; + const rowNode = gridRef.current.api.getRowNode( + getRowNodeId(delta.market) + ); + + if (rowNode) { + const updatedData = produce( + rowNode.data.data, + (draft: Markets_markets_data) => assign(draft, delta) + ); + if (updatedData !== rowNode.data.data) { + update.push({ ...rowNode.data, data: delta }); + } + } /* else { + add.push(d); + }*/ + // async transaction for optimal handling of high grequency updates + if (update.length || add.length) { + gridRef.current.api.applyTransactionAsync({ + update, + add, + addIndex: 0, + }); + } + } + } + }); + }, [client, initialized]); return ( {(data) => ( push(`${pathname}/${id}?portfolio=orders&trade=orderbook`) @@ -23,15 +80,8 @@ const Markets = () => { ); }; -const TwoMarkets = () => ( - <> -
- -
-
- -
- -); +export default Markets; -export default TwoMarkets; +// const TwoMarkets = () => (<>
) + +// export default TwoMarkets; diff --git a/libs/market-list/src/lib/market-list-table.tsx b/libs/market-list/src/lib/market-list-table.tsx index 4275dfeef..a3a567a46 100644 --- a/libs/market-list/src/lib/market-list-table.tsx +++ b/libs/market-list/src/lib/market-list-table.tsx @@ -12,23 +12,24 @@ interface MarketListTableProps { onRowClicked: (marketId: string) => void; } +export const getRowNodeId = (data: { id: string }) => data.id; + export const MarketListTable = forwardRef( ({ markets, onRowClicked }, ref) => { - const [initialMarkets] = useState(markets); - const getRowNodeId = (data: Markets_markets) => data.id; - return ( onRowClicked(data.id)} + onRowClicked={({ data }: { data: Markets_markets }) => + onRowClicked(data.id) + } components={{ PriceCell }} > ( { ssr: false } ); -export const AgGridThemed = React.forwardRef< - AgGridReact, - (AgGridReactProps | AgReactUiProps) & { - style?: React.CSSProperties; - className?: string; - } ->(({ style, className, ...props }, ref) => { +export const AgGridThemed = ({ + style, + className, + gridRef, + ...props +}: (AgGridReactProps | AgReactUiProps) & { + style?: React.CSSProperties; + className?: string; + gridRef?: React.ForwardedRef; +}) => { const theme = React.useContext(ThemeContext); return (
{theme === 'dark' ? ( - + ) : ( - + )}
); -}); +}; diff --git a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx index a20c841ca..1fcc41129 100644 --- a/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx +++ b/libs/ui-toolkit/src/components/ag-grid/ag-grid-dynamic.tsx @@ -10,7 +10,7 @@ import type { type Props = (AgGridReactProps | AgReactUiProps) & { style?: React.CSSProperties; className?: string; - ref?: React.Ref; + gridRef?: React.Ref; }; // https://stackoverflow.com/questions/69433673/nextjs-reactdomserver-does-not-yet-support-suspense @@ -23,6 +23,6 @@ const AgGridDynamicInternal = dynamic( } ); -export const AgGridDynamic = React.forwardRef((props, ref) => ( - -)); +export const AgGridDynamic = React.forwardRef( + (props, ref) => +); diff --git a/package.json b/package.json index e1f981a5b..11de924dd 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "ag-grid-community": "^27.0.1", "ag-grid-react": "^27.0.1", "apollo": "^2.33.9", + "assign-deep": "^1.0.1", "autoprefixer": "^10.4.2", "bignumber.js": "^9.0.2", "classnames": "^2.3.1", diff --git a/yarn.lock b/yarn.lock index fecbd993b..0cd26c6d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6727,11 +6727,23 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assign-deep@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/assign-deep/-/assign-deep-1.0.1.tgz#b6d21d74e2f28bf6592e4c0c541bed6ab59c5f27" + integrity sha512-CSXAX79mibneEYfqLT5FEmkqR5WXF+xDRjgQQuVf6wSCXCYU8/vHttPidNar7wJ5BFmKAo8Wei0rCtzb+M/yeA== + dependencies: + assign-symbols "^2.0.2" + assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= +assign-symbols@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-2.0.2.tgz#0fb9191dd9d617042746ecfc354f3a3d768a0c98" + integrity sha512-9sBQUQZMKFKcO/C3Bo6Rx4CQany0R0UeVcefNGRRdW2vbmaMOhV1sbmlXcQLcD56juLXbSGTBm0GGuvmrAF8pA== + ast-types-flow@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"