diff --git a/apps/trading/.eslintrc.json b/apps/trading/.eslintrc.json index 49206a05e..2548ff963 100644 --- a/apps/trading/.eslintrc.json +++ b/apps/trading/.eslintrc.json @@ -19,15 +19,7 @@ }, { "files": ["*.js", "*.jsx"], - "rules": { - "unicorn/filename-case": [ - "error", - { - "case": "kebabCase", - "ignore": ["react-singleton-hook/**/*.js"] - } - ] - } + "rules": {} } ], "env": { diff --git a/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js b/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js deleted file mode 100644 index 54b4909b3..000000000 --- a/apps/trading/hooks/react-singleton-hook/components/SingleItemContainer.js +++ /dev/null @@ -1,19 +0,0 @@ -import { useLayoutEffect, useRef } from 'react'; - -export const SingleItemContainer = ({ initValue, useHookBody, applyStateChange }) => { - const lastState = useRef(initValue); - if (typeof useHookBody !== 'function') { - throw new Error(`function expected as hook body parameter. got ${typeof useHookBody}`); - } - const val = useHookBody(); - - //useLayoutEffect is safe from SSR perspective because SingleItemContainer should never be rendered on server - useLayoutEffect(() => { - if (lastState.current !== val) { - lastState.current = val; - applyStateChange(val); - } - }, [applyStateChange, val]); - - return null; -}; diff --git a/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js b/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js deleted file mode 100644 index c6bd2593b..000000000 --- a/apps/trading/hooks/react-singleton-hook/components/SingletonHooksContainer.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { SingleItemContainer } from './SingleItemContainer'; -import { mount } from '../utils/env'; -import { warning } from '../utils/warning'; - -let SingletonHooksContainerMounted = false; -let SingletonHooksContainerRendered = false; -let SingletonHooksContainerMountedAutomatically = false; - -let mountQueue = []; -const mountIntoContainerDefault = (item) => { - mountQueue.push(item); - return () => { - mountQueue = mountQueue.filter(i => i !== item); - } -}; -let mountIntoContainer = mountIntoContainerDefault; - -export const SingletonHooksContainer = () => { - SingletonHooksContainerRendered = true; - useEffect(() => { - if (SingletonHooksContainerMounted) { - warning('SingletonHooksContainer is mounted second time. ' - + 'You should mount SingletonHooksContainer before any other component and never unmount it.' - + 'Alternatively, dont use SingletonHooksContainer it at all, we will handle that for you.'); - } - SingletonHooksContainerMounted = true; - }, []); - - const [hooks, setHooks] = useState([]); - - useEffect(() => { - mountIntoContainer = item => { - setHooks(hooks => [...hooks, item]); - return () => { - setHooks(hooks => hooks.filter(i => i !== item)); - } - } - setHooks(mountQueue); - }, []); - - return <>{hooks.map((h, i) => )}; -}; - - -export const addHook = hook => { - if (!SingletonHooksContainerRendered && !SingletonHooksContainerMountedAutomatically) { - SingletonHooksContainerMountedAutomatically = true; - mount(SingletonHooksContainer); - } - return mountIntoContainer(hook); -}; - -export const resetLocalStateForTests = () => { - SingletonHooksContainerMounted = false; - SingletonHooksContainerRendered = false; - SingletonHooksContainerMountedAutomatically = false; - mountQueue = []; - mountIntoContainer = mountIntoContainerDefault; -}; diff --git a/apps/trading/hooks/react-singleton-hook/index.js b/apps/trading/hooks/react-singleton-hook/index.js deleted file mode 100644 index b5494415e..000000000 --- a/apps/trading/hooks/react-singleton-hook/index.js +++ /dev/null @@ -1,14 +0,0 @@ -import { singletonHook } from './singletonHook'; -import { SingletonHooksContainer } from './components/SingletonHooksContainer'; - -export { - singletonHook, - SingletonHooksContainer -}; - -const ReactSingletonHook = { - singletonHook, - SingletonHooksContainer -}; - -export default ReactSingletonHook; diff --git a/apps/trading/hooks/react-singleton-hook/singletonHook.js b/apps/trading/hooks/react-singleton-hook/singletonHook.js deleted file mode 100644 index 30d9e0130..000000000 --- a/apps/trading/hooks/react-singleton-hook/singletonHook.js +++ /dev/null @@ -1,51 +0,0 @@ -import { useEffect, useState } from 'react'; -import { addHook } from './components/SingletonHooksContainer'; -import { batch } from './utils/env'; - -export const singletonHook = (initValue, useHookBody, unmount = false) => { - let mounted = false; - let removeHook = undefined - let initStateCalculated = false; - let lastKnownState = undefined; - let consumers = []; - - const applyStateChange = (newState) => { - lastKnownState = newState; - batch(() => consumers.forEach(c => c(newState))); - }; - - const stateInitializer = () => { - if (!initStateCalculated) { - lastKnownState = typeof initValue === 'function' ? initValue() : initValue; - initStateCalculated = true; - } - return lastKnownState; - }; - - return () => { - const [state, setState] = useState(stateInitializer); - - useEffect(() => { - if (!mounted) { - mounted = true; - removeHook = addHook({ initValue, useHookBody, applyStateChange }); - } - - consumers.push(setState); - if (lastKnownState !== state) { - setState(lastKnownState); - } - return () => { - consumers.splice(consumers.indexOf(setState), 1); - if (consumers.length === 0 && unmount) { - removeHook(); - mounted = false; - } - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return state; - }; -}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/env.js b/apps/trading/hooks/react-singleton-hook/utils/env.js deleted file mode 100644 index 6c539c932..000000000 --- a/apps/trading/hooks/react-singleton-hook/utils/env.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -/* eslint-disable import/no-unresolved */ -import { unstable_batchedUpdates, render } from 'react-dom'; -import { warning } from './warning'; - -// from https://github.com/purposeindustries/window-or-global/blob/master/lib/index.js -// avoid direct usage of 'window' because `window is not defined` error might happen in babel-node -const globalObject = (typeof self === 'object' && self.self === self && self) - || (typeof global === 'object' && global.global === global && global) - || this; - - -export const batch = cb => unstable_batchedUpdates(cb); -export const mount = C => { - if (globalObject.document && globalObject.document.createElement) { - render(, globalObject.document.createElement('div')); - } else { - warning('Can not mount SingletonHooksContainer on server side. ' - + 'Did you manage to run useEffect on server? ' - + 'Please mount SingletonHooksContainer into your components tree manually.'); - } -}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/env.native.js b/apps/trading/hooks/react-singleton-hook/utils/env.native.js deleted file mode 100644 index 8ce8c83ee..000000000 --- a/apps/trading/hooks/react-singleton-hook/utils/env.native.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable import/no-unresolved */ -import { unstable_batchedUpdates } from 'react-native'; -import { warning } from './warning'; - -export const batch = cb => unstable_batchedUpdates(cb); -export const mount = C => { - warning('Can not mount SingletonHooksContainer with react native.' - + 'Please mount SingletonHooksContainer into your components tree manually.'); -}; diff --git a/apps/trading/hooks/react-singleton-hook/utils/warning.js b/apps/trading/hooks/react-singleton-hook/utils/warning.js deleted file mode 100644 index c644a7070..000000000 --- a/apps/trading/hooks/react-singleton-hook/utils/warning.js +++ /dev/null @@ -1,6 +0,0 @@ - -export const warning = (message) => { - if (console && console.warn) { - console.warn(message); - } -}; diff --git a/apps/trading/hooks/use-markets.ts b/apps/trading/hooks/use-markets.ts deleted file mode 100644 index aa54751d0..000000000 --- a/apps/trading/hooks/use-markets.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { gql, useApolloClient } from '@apollo/client'; -import produce from 'immer'; -import { - Markets, - Markets_markets, - MarketDataSub, - MarketDataSub_marketData, -} from '@vegaprotocol/graphql'; -import { useCallback, useEffect, useState } from 'react'; - -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 - } - } -`; - -interface UseMarkets { - markets: Markets_markets[]; - error: Error | null; - loading: boolean; -} - -export const useMarkets = (updateCallback?: (data: MarketDataSub_marketData) => void): UseMarkets => { - const client = useApolloClient(); - const [markets, setMarkets] = useState([]); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - const mergeMarketData = useCallback((update: MarketDataSub_marketData) => { - setMarkets((curr) => - produce(curr, (draft) => { - const index = draft.findIndex((m) => m.id === update.market.id); - if (index !== -1) { - draft[index].data = update; - } - }) - ); - }, []); - - // Make initial fetch - useEffect(() => { - 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); - } - }; - - fetchOrders(); - }, [mergeMarketData, client]); - - // Start subscription - useEffect(() => { - const sub = client - // This data callback will unfortunately be called separately with an update for every market, - // perhaps we should batch this somehow... - .subscribe({ - query: MARKET_DATA_SUB, - }) - .subscribe(({ data }) => { - mergeMarketData(data.marketData); - }); - - return () => { - if (sub) { - sub.unsubscribe(); - } - }; - }, [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 704a78305..28bfc35b4 100644 --- a/apps/trading/pages/markets/index.page.tsx +++ b/apps/trading/pages/markets/index.page.tsx @@ -7,11 +7,11 @@ import { AsyncRenderer } from '../../components/async-renderer'; import { MarketListTable, getRowNodeId } from '@vegaprotocol/market-list'; import { Markets_markets, - Markets_markets_data + Markets_markets_data, + MarketsDataProviderCallbackArg, + marketsDataProvider, } 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 = () => { @@ -24,45 +24,48 @@ const Markets = () => { 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[] = []; + return marketsDataProvider( + client, + ({ data, error, loading, delta }: MarketsDataProviderCallbackArg) => { + 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) + // split into updates and adds + if (!gridRef.current) return; + const rowNode = gridRef.current.api.getRowNode( + getRowNodeId(delta.market) ); - if (updatedData !== rowNode.data.data) { - update.push({ ...rowNode.data, data: delta }); - } - } /* else { + + 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, - }); + // 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 ( diff --git a/libs/graphql/src/data-providers/index.ts b/libs/graphql/src/data-providers/index.ts new file mode 100644 index 000000000..8da2b0152 --- /dev/null +++ b/libs/graphql/src/data-providers/index.ts @@ -0,0 +1 @@ +export * from './markets-data-provider'; diff --git a/apps/trading/data-providers/markets-data-provider.ts b/libs/graphql/src/data-providers/markets-data-provider.ts similarity index 63% rename from apps/trading/data-providers/markets-data-provider.ts rename to libs/graphql/src/data-providers/markets-data-provider.ts index 28c97ff3b..6309de1db 100644 --- a/apps/trading/data-providers/markets-data-provider.ts +++ b/libs/graphql/src/data-providers/markets-data-provider.ts @@ -2,12 +2,12 @@ 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 } from '../__generated__/Markets'; + import { - Markets, - Markets_markets, MarketDataSub, MarketDataSub_marketData, -} from '@vegaprotocol/graphql'; +} from '../__generated__/MarketDataSub'; const MARKET_DATA_FRAGMENT = gql` fragment MarketDataFields on MarketData { @@ -57,27 +57,30 @@ const MARKET_DATA_SUB = gql` } `; -export interface CallbackArg { - data?: Markets_markets[]; +export interface MarketsDataProviderCallbackArg { + data: Markets_markets[] | null; error?: Error; loading: boolean; delta?: MarketDataSub_marketData; } -export interface Callback { - (arg: CallbackArg): void; +export interface MarketsDataProviderCallback { + (arg: MarketsDataProviderCallbackArg): void; } -const callbacks: Callback[] = []; +const callbacks: MarketsDataProviderCallback[] = []; const updateQueue: MarketDataSub_marketData[] = []; -let data: Markets_markets[] = undefined; -let error: Error = undefined; +let data: Markets_markets[] | null = null; +let error: Error | undefined = undefined; let loading = false; -let client: ApolloClient = undefined; -let subscription: Subscription = undefined; +let client: ApolloClient | undefined = undefined; +let subscription: Subscription | undefined = undefined; -const notify = (callback, delta?: MarketDataSub_marketData) => { +const notify = ( + callback: MarketsDataProviderCallback, + delta?: MarketDataSub_marketData +) => { callback({ data, error, @@ -90,7 +93,13 @@ const notifyAll = (delta?: MarketDataSub_marketData) => { callbacks.forEach((callback) => notify(callback, delta)); }; -const update = (draft: Markets_markets[], delta: MarketDataSub_marketData) => { +const update = ( + draft: Markets_markets[] | null, + delta: MarketDataSub_marketData +) => { + if (!draft) { + return; + } const index = draft.findIndex((m) => m.id === delta.market.id); if (index !== -1) { draft[index].data = delta; @@ -103,19 +112,29 @@ const initialize = async () => { return; } loading = true; - error = null; + error = undefined; notifyAll(); + if (!client) { + return; + } subscription = client .subscribe({ query: MARKET_DATA_SUB, }) .subscribe(({ data: delta }) => { + if (!delta) { + return; + } if (loading) { updateQueue.push(delta.marketData); } else { - data = produce(data, (draft) => { + const newData = produce(data, (draft) => { update(draft, delta.marketData); }); + if (newData === data) { + return; + } + data = newData; notifyAll(delta.marketData); } }); @@ -124,15 +143,18 @@ const initialize = async () => { query: MARKETS_QUERY, }); data = res.data.markets; - if (updateQueue) { + if (updateQueue && updateQueue.length > 0) { data = produce(data, (draft) => { while (updateQueue.length) { - update(draft, updateQueue.shift()); + const delta = updateQueue.shift(); + if (delta) { + update(draft, delta); + } } }); } } catch (e) { - error = e; + error = e as Error; subscription.unsubscribe(); subscription = undefined; } finally { @@ -141,18 +163,23 @@ const initialize = async () => { } }; -const unsubscribe = (callback: Callback) => { +const unsubscribe = (callback: MarketsDataProviderCallback) => { callbacks.splice(callbacks.indexOf(callback), 1); if (callbacks.length === 0) { - subscription.unsubscribe(); - subscription = undefined; - data = undefined; + if (subscription) { + subscription.unsubscribe(); + subscription = undefined; + } + data = null; error = undefined; loading = false; } }; -export const subscribe = (c: ApolloClient, callback) => { +export const marketsDataProvider = ( + c: ApolloClient, + callback: MarketsDataProviderCallback +) => { if (!client) { client = c; } diff --git a/libs/graphql/src/index.ts b/libs/graphql/src/index.ts index 59c3a0077..37e47e81f 100644 --- a/libs/graphql/src/index.ts +++ b/libs/graphql/src/index.ts @@ -14,3 +14,5 @@ export * from './__generated__/Orders'; export * from './__generated__/OrderSub'; export * from './__generated__/PartyAssetsQuery'; export * from './__generated__/ProposalsQuery'; + +export * from './data-providers';