refactor: env provider to use zustand

This commit is contained in:
Matthew Russell
2023-02-15 11:47:13 -08:00
parent b7426e73d8
commit da66da170a
3 changed files with 249 additions and 12 deletions
+104 -12
View File
@@ -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 <DynamicLoader />;
}
return (
<HashRouter>
<EnvironmentProvider>
<AppBody {...props} />
</EnvironmentProvider>
{/* <AppBody {...props} /> */}
<Test />
<button onClick={() => setOpen(true)}>Status</button>
<NodeSwitcher open={open} setOpen={setOpen} />
</HashRouter>
);
}
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 (
<Dialog open={open} onChange={setOpen}>
<table className="w-full">
<thead>
<tr>
<th className="text-left">node</th>
<th className="text-right">response time</th>
<th className="text-right">block height</th>
</tr>
</thead>
<tbody>
{nodes.map((node) => {
const client = clients[node];
if (!client) return null;
return (
<tr key={node} onClick={() => setUrl(node)}>
<ApolloProvider client={client}>
<Row url={node} />
</ApolloProvider>
</tr>
);
})}
</tbody>
</table>
<div>
Custom
<input
value={customUrl}
onChange={(e) => setCustomUrl(e.target.value)}
/>
{/* <button onClick={}>Check</button> */}
</div>
</Dialog>
);
};
const Row = ({ url }: { url: string }) => {
const [time, setTime] = useState<number>();
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 (
<>
<td>{url}</td>
<td className="text-right">{time ? time.toFixed(2) + 'ms' : 'n/a'}</td>
<td className="text-right">{data?.statistics.blockHeight || '-'}</td>
</>
);
};
const Test = () => {
const env = useEnvironment2((store) => ({
url: store.url,
configUrl: store.configUrl,
nodes: store.nodes,
status: store.status,
}));
return (
<div>
<pre>{JSON.stringify(env, null, 2)}</pre>
</div>
);
};
export default VegaTradingApp;
const MaybeConnectEagerly = () => {
+1
View File
@@ -1,3 +1,4 @@
export * from './use-environment';
export * from './use-environment-2';
export * from './use-links';
export * from './use-node-health';
@@ -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<typeof createClient>;
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<void>;
}
export const useEnvironment2 = create<Env & Actions>((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<string[]> => {
const res = await fetch(url);
const cfg = await res.json();
return cfg.hosts;
};
const findNode = (clients: ClientCollection): Promise<string | null> => {
const tests = Object.entries(clients).map((args) => testNode(...args));
return Promise.race(tests);
};
const testNode = async (
url: string,
client: Client
): Promise<string | null> => {
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<StatisticsQuery>({
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<BlockTimeSubscription>({
query: BlockTimeDocument,
errorPolicy: 'all',
})
.subscribe({
next: () => {
resolve(true);
sub.unsubscribe();
},
error: () => {
resolve(false);
sub.unsubscribe();
},
});
});
};