From da66da170a5aa97eea829e508444812af62d1b20 Mon Sep 17 00:00:00 2001 From: Matthew Russell Date: Thu, 2 Feb 2023 20:16:13 -0800 Subject: [PATCH] refactor: env provider to use zustand --- apps/trading/pages/_app.page.tsx | 116 ++++++++++++-- libs/environment/src/hooks/index.ts | 1 + .../src/hooks/use-environment-2.ts | 144 ++++++++++++++++++ 3 files changed, 249 insertions(+), 12 deletions(-) create mode 100644 libs/environment/src/hooks/use-environment-2.ts diff --git a/apps/trading/pages/_app.page.tsx b/apps/trading/pages/_app.page.tsx index b823cceee..a0266fce8 100644 --- a/apps/trading/pages/_app.page.tsx +++ b/apps/trading/pages/_app.page.tsx @@ -17,10 +17,15 @@ import { useEthWithdrawApprovalsManager, } from '@vegaprotocol/web3'; import { + clients, EnvironmentProvider, envTriggerMapping, Networks, + NodeSwitcherDialog, useEnvironment, + useEnvironment2, + useInitializeEnv, + useStatisticsQuery, } from '@vegaprotocol/environment'; import { AppLoader, Web3Provider } from '../components/app-loader'; import './styles.css'; @@ -35,6 +40,8 @@ import { Connectors } from '../lib/vega-connectors'; import { ViewingBanner } from '../components/viewing-banner'; import { Banner } from '../components/banner'; import classNames from 'classnames'; +import { Dialog } from '@vegaprotocol/ui-toolkit'; +import { ApolloProvider } from '@apollo/client'; const DEFAULT_TITLE = t('Welcome to Vega trading!'); @@ -117,28 +124,113 @@ const DynamicLoader = dynamic( ); function VegaTradingApp(props: AppProps) { - const [mounted, setMounted] = useState(false); + const [open, setOpen] = useState(true); + const status = useEnvironment2((store) => store.status); + useInitializeEnv(); - // Hash router requires access to the document object. At compile time that doesn't exist - // so we need to ensure client side rendering only from this point onwards in - // the component tree - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted) { + if (status === 'default' || status === 'pending') { return ; } return ( - - - + {/* */} + + + ); } +const NodeSwitcher = ({ + open, + setOpen, +}: { + open: boolean; + setOpen: (x: boolean) => void; +}) => { + const [customUrl, setCustomUrl] = useState(''); + const { status, nodes, setUrl } = useEnvironment2((store) => ({ + status: store.status, + nodes: store.nodes, + setUrl: store.setUrl, + })); + + return ( + + + + + + + + + + + {nodes.map((node) => { + const client = clients[node]; + + if (!client) return null; + + return ( + setUrl(node)}> + + + + + ); + })} + +
noderesponse timeblock height
+
+ Custom + setCustomUrl(e.target.value)} + /> + {/* */} +
+
+ ); +}; + +const Row = ({ url }: { url: string }) => { + const [time, setTime] = useState(); + const { data } = useStatisticsQuery({ + pollInterval: 3000, + }); + + useEffect(() => { + const requestUrl = new URL(url); + const requests = window.performance.getEntriesByName(requestUrl.href); + const { duration } = + (requests.length && requests[requests.length - 1]) || {}; + setTime(duration); + }, [url]); + + return ( + <> + {url} + {time ? time.toFixed(2) + 'ms' : 'n/a'} + {data?.statistics.blockHeight || '-'} + + ); +}; + +const Test = () => { + const env = useEnvironment2((store) => ({ + url: store.url, + configUrl: store.configUrl, + nodes: store.nodes, + status: store.status, + })); + return ( +
+
{JSON.stringify(env, null, 2)}
+
+ ); +}; + export default VegaTradingApp; const MaybeConnectEagerly = () => { diff --git a/libs/environment/src/hooks/index.ts b/libs/environment/src/hooks/index.ts index c1c468099..6961f5a23 100644 --- a/libs/environment/src/hooks/index.ts +++ b/libs/environment/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './use-environment'; +export * from './use-environment-2'; export * from './use-links'; export * from './use-node-health'; diff --git a/libs/environment/src/hooks/use-environment-2.ts b/libs/environment/src/hooks/use-environment-2.ts new file mode 100644 index 000000000..b765beccf --- /dev/null +++ b/libs/environment/src/hooks/use-environment-2.ts @@ -0,0 +1,144 @@ +import { LocalStorage } from '@vegaprotocol/react-helpers'; +import { useEffect } from 'react'; +import { create } from 'zustand'; +import { createClient } from '@vegaprotocol/apollo-client'; +import type { + BlockTimeSubscription, + StatisticsQuery, +} from '../utils/__generated__/Node'; +import { + BlockTimeDocument, + StatisticsDocument, +} from '../utils/__generated__/Node'; + +type Client = ReturnType; +type ClientCollection = { + [node: string]: Client; +}; +export const clients: ClientCollection = {}; + +interface Env { + url: string; + configUrl: string; + nodes: string[]; + status: 'default' | 'pending' | 'success' | 'failed'; +} + +interface Actions { + setUrl: (url: string) => void; + initialize: () => Promise; +} + +export const useEnvironment2 = create((set, get) => ({ + url: process.env['NX_VEGA_URL'] || '', + vegaEnv: process.env['NX_VEGA_ENV'] || '', + configUrl: process.env['NX_VEGA_CONFIG_URL'] || '', + nodes: [], + status: 'default', + setUrl: (url) => { + set({ url }); + }, + initialize: async () => { + const state = get(); + if (state.status === 'pending') return; + const storedUrl = LocalStorage.getItem('vega_url'); + set({ status: 'pending' }); + const nodes = await fetchConfig(state.configUrl); + set({ nodes }); + + // create client and store instances + nodes.forEach((url) => { + clients[url] = createClient({ + url, + cacheConfig: undefined, + retry: false, + connectToDevTools: false, + }); + }); + + // if (storedUrl) { + // set({ url: storedUrl, status: 'success' }); + // } else { + const url = await findNode(clients); + set({ + status: url ? 'success' : 'failed', + url: url ? url : '', + }); + // } + }, +})); + +export const useInitializeEnv = () => { + const { initialize, ...env } = useEnvironment2(); + + useEffect(() => { + if (env.status === 'default') { + initialize(); + } + }, [env.status, initialize]); +}; + +const fetchConfig = async (url: string): Promise => { + const res = await fetch(url); + const cfg = await res.json(); + return cfg.hosts; +}; + +const findNode = (clients: ClientCollection): Promise => { + const tests = Object.entries(clients).map((args) => testNode(...args)); + return Promise.race(tests); +}; + +const testNode = async ( + url: string, + client: Client +): Promise => { + return null; + try { + const results = await Promise.all([ + testQuery(client), + testSubscription(client), + ]); + if (results[0] && results[1]) { + return url; + } + return null; + } catch (err) { + console.warn(`tests failed for ${url}`); + return null; + } +}; + +const testQuery = async (client: Client) => { + try { + const result = await client.query({ + query: StatisticsDocument, + }); + if (!result || result.error) { + return false; + } + return true; + } catch (err) { + return false; + } +}; + +const testSubscription = (client: Client) => { + return new Promise((resolve) => { + const sub = client + .subscribe({ + query: BlockTimeDocument, + errorPolicy: 'all', + }) + .subscribe({ + next: () => { + resolve(true); + sub.unsubscribe(); + }, + error: () => { + resolve(false); + sub.unsubscribe(); + }, + }); + }); +};