From 92315181e1c4c79b20889eccbbdd4cb8a0c7d6bc Mon Sep 17 00:00:00 2001 From: Botond Date: Wed, 6 Jul 2022 19:15:10 +0100 Subject: [PATCH] feat: wip refactor config hook to run on init --- .../src/components/network-loader/index.ts | 1 - .../network-loader/network-loader.tsx | 6 +- .../node-switcher-dialog.tsx | 2 + .../node-switcher/node-switcher.tsx | 26 ++-- .../environment/src/hooks/use-config.spec.tsx | 28 ++-- libs/environment/src/hooks/use-config.tsx | 133 +++++++++++------- .../environment/src/hooks/use-environment.tsx | 10 +- libs/environment/src/types.ts | 17 ++- .../environment/src/utils/initialize-node.tsx | 84 +++-------- libs/environment/src/utils/request-node.ts | 76 ++++++++++ libs/environment/src/utils/validate-node.tsx | 61 ++++---- 11 files changed, 266 insertions(+), 178 deletions(-) delete mode 100644 libs/environment/src/components/network-loader/index.ts create mode 100644 libs/environment/src/utils/request-node.ts diff --git a/libs/environment/src/components/network-loader/index.ts b/libs/environment/src/components/network-loader/index.ts deleted file mode 100644 index 32e02ef13..000000000 --- a/libs/environment/src/components/network-loader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './network-loader'; diff --git a/libs/environment/src/components/network-loader/network-loader.tsx b/libs/environment/src/components/network-loader/network-loader.tsx index 304f6a90e..505cec708 100644 --- a/libs/environment/src/components/network-loader/network-loader.tsx +++ b/libs/environment/src/components/network-loader/network-loader.tsx @@ -11,7 +11,6 @@ import { } from '@vegaprotocol/ui-toolkit'; import { t } from '@vegaprotocol/react-helpers'; import { useEnvironment } from '../../hooks'; -import type { ConfigStatus } from '../../types'; type MessageComponentProps = { children: ReactNode; @@ -44,11 +43,10 @@ const Error = ({ children, showTryAgain }: ErrorComponentProps) => ( ); type StatusComponentProps = { - status: ConfigStatus; children?: ReactNode; }; -const StatusComponent = ({ status, children }: StatusComponentProps) => { +const StatusComponent = ({ children }: StatusComponentProps) => { switch (status) { case 'error-loading-config': return ( @@ -137,7 +135,7 @@ export function NetworkLoader({ }: NetworkLoaderProps) { // this is to prevent an error rendering callouts on the server side const [canShowCallout, setShowCallout] = useState(false); - const { configStatus, VEGA_URL } = useEnvironment(); + const { VEGA_URL } = useEnvironment(); const client = useMemo(() => { if (VEGA_URL) { diff --git a/libs/environment/src/components/node-switcher-dialog/node-switcher-dialog.tsx b/libs/environment/src/components/node-switcher-dialog/node-switcher-dialog.tsx index 31a672c8a..205852324 100644 --- a/libs/environment/src/components/node-switcher-dialog/node-switcher-dialog.tsx +++ b/libs/environment/src/components/node-switcher-dialog/node-switcher-dialog.tsx @@ -9,6 +9,7 @@ type NodeSwitcherDialogProps = ComponentProps & { export const NodeSwitcherDialog = ({ config, + initialErrorType, dialogOpen, setDialogOpen, onConnect, @@ -17,6 +18,7 @@ export const NodeSwitcherDialog = ({ { onConnect(url); setDialogOpen(false); diff --git a/libs/environment/src/components/node-switcher/node-switcher.tsx b/libs/environment/src/components/node-switcher/node-switcher.tsx index f48877c24..fea2fee4f 100644 --- a/libs/environment/src/components/node-switcher/node-switcher.tsx +++ b/libs/environment/src/components/node-switcher/node-switcher.tsx @@ -13,10 +13,11 @@ import { getIsNodeLoading, getIsNodeDisabled, getIsFormDisabled, - getErrorMessage, + getErrorByData, + getErrorByType, } from '../../utils/validate-node'; import { CUSTOM_NODE_KEY } from '../../types'; -import type { Configuration, NodeData } from '../../types'; +import type { Configuration, NodeData, ErrorType } from '../../types'; import { LayoutRow } from './layout-row'; import { LayoutCell } from './layout-cell'; import { NodeError } from './node-error'; @@ -25,6 +26,7 @@ import { NodeStats } from './node-stats'; type NodeSwitcherProps = { error?: string; config: Configuration; + initialErrorType?: ErrorType; onConnect: (url: string) => void; }; @@ -39,8 +41,9 @@ const getHighestBlock = (state: Record) => { }, 0); }; -export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => { +export const NodeSwitcher = ({ config, initialErrorType, onConnect }: NodeSwitcherProps) => { const { VEGA_ENV, VEGA_URL } = useEnvironment(); + const [networkError, setNetworkError] = useState(getErrorByType(initialErrorType, VEGA_ENV, VEGA_URL)); const [customNodeText, setCustomNodeText] = useState(''); const [nodeRadio, setNodeRadio] = useState( getDefaultNode(config.hosts, VEGA_URL) @@ -61,7 +64,7 @@ export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => { VEGA_ENV, state ); - const currentNodeError = getErrorMessage( + const currentNodeError = getErrorByData( VEGA_ENV, nodeRadio && customNode && customNode === customNodeText ? state[nodeRadio] @@ -70,7 +73,7 @@ export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => { return (
- +
onSubmit(nodeRadio)}>

{t('Select a GraphQL node to connect to:')} @@ -85,7 +88,10 @@ export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => { setNodeRadio(value)} + onChange={(value) => { + setNodeRadio(value) + setNetworkError(null); + }} >

{config.hosts.map((node, index) => ( @@ -137,12 +143,14 @@ export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => { value={customNodeText} hasError={ !!customNodeText && - (!!currentNodeError.headline || - !!currentNodeError.message) + !!(currentNodeError?.headline || currentNodeError?.message) } onChange={(e) => setCustomNodeText(e.target.value)} /> - setCustomNode(customNodeText)}> + { + setNetworkError(null); + setCustomNode(customNodeText) + }}> {getIsNodeLoading(state[CUSTOM_NODE_KEY]) ? t('Checking') : t('Check')} diff --git a/libs/environment/src/hooks/use-config.spec.tsx b/libs/environment/src/hooks/use-config.spec.tsx index ed9a54d0c..41f3aa398 100644 --- a/libs/environment/src/hooks/use-config.spec.tsx +++ b/libs/environment/src/hooks/use-config.spec.tsx @@ -57,10 +57,12 @@ const noop = () => {}; global.fetch = jest.fn(); const mockUpdate = jest.fn(); +const onConnectionError = jest.fn(); beforeEach(() => { jest.useFakeTimers(); mockUpdate.mockClear(); + onConnectionError.mockClear(); window.localStorage.clear(); // @ts-ignore typescript doesn't recognise the mocked instance @@ -82,7 +84,7 @@ describe('useConfig hook', () => { ...mockEnvironment, VEGA_URL: 'https://some.url/query', }; - const { result } = renderHook(() => useConfig(mockEnvWithUrl, mockUpdate)); + const { result } = renderHook(() => useConfig(mockEnvWithUrl, mockUpdate, onConnectionError)); expect(fetch).not.toHaveBeenCalled(); expect(mockUpdate).not.toHaveBeenCalled(); @@ -98,7 +100,7 @@ describe('useConfig hook', () => { ]; const { result, waitForNextUpdate } = renderHook(() => - useConfig(mockEnvironment, mockUpdate) + useConfig(mockEnvironment, mockUpdate, onConnectionError) ); await waitForNextUpdate(); @@ -128,7 +130,7 @@ describe('useConfig hook', () => { }); const { result, waitForNextUpdate } = renderHook(() => - useConfig(mockEnvironment, mockUpdate) + useConfig(mockEnvironment, mockUpdate, onConnectionError) ); await waitForNextUpdate(); @@ -153,7 +155,7 @@ describe('useConfig hook', () => { ); const { result, waitForNextUpdate } = renderHook(() => - useConfig(mockEnvironment, mockUpdate) + useConfig(mockEnvironment, mockUpdate, onConnectionError) ); await waitForNextUpdate(); @@ -174,7 +176,7 @@ describe('useConfig hook', () => { }); const { result, waitForNextUpdate } = renderHook(() => - useConfig(mockEnvironment, mockUpdate) + useConfig(mockEnvironment, mockUpdate, onConnectionError) ); await waitForNextUpdate(); @@ -196,7 +198,7 @@ describe('useConfig hook', () => { }); const { result, waitForNextUpdate } = renderHook(() => - useConfig(mockEnvironment, mockUpdate) + useConfig(mockEnvironment, mockUpdate, onConnectionError) ); await waitForNextUpdate(); @@ -219,7 +221,7 @@ describe('useConfig hook', () => { ); const { result, waitForNextUpdate } = renderHook(() => - useConfig(mockEnvironment, mockUpdate) + useConfig(mockEnvironment, mockUpdate, onConnectionError) ); await waitForNextUpdate(); @@ -229,7 +231,7 @@ describe('useConfig hook', () => { }); it('caches the list of networks', async () => { - const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); + const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate, onConnectionError)); await run1.waitForNextUpdate(); jest.runAllTimers(); @@ -241,7 +243,7 @@ describe('useConfig hook', () => { // @ts-ignore typescript doesn't recognise the mocked instance fetch.mockClear(); - const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); + const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate, onConnectionError)); jest.runAllTimers(); await run2.waitForNextUpdate(); @@ -251,7 +253,7 @@ describe('useConfig hook', () => { }); it('caches the list of networks between runs', async () => { - const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); + const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate, onConnectionError)); await run1.waitForNextUpdate(); jest.runAllTimers(); @@ -263,7 +265,7 @@ describe('useConfig hook', () => { // @ts-ignore typescript doesn't recognise the mocked instance fetch.mockClear(); - const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); + const run2 = renderHook(() => useConfig(mockEnvironment, mockUpdate, onConnectionError)); jest.runAllTimers(); await run2.waitForNextUpdate(); @@ -276,7 +278,7 @@ describe('useConfig hook', () => { window.localStorage.setItem(LOCAL_STORAGE_NETWORK_KEY, '{not:{valid:{json'); const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(noop); - const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); + const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate, onConnectionError)); await run1.waitForNextUpdate(); jest.runAllTimers(); @@ -296,7 +298,7 @@ describe('useConfig hook', () => { ); const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(noop); - const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate)); + const run1 = renderHook(() => useConfig(mockEnvironment, mockUpdate, onConnectionError)); await run1.waitForNextUpdate(); jest.runAllTimers(); diff --git a/libs/environment/src/hooks/use-config.tsx b/libs/environment/src/hooks/use-config.tsx index 2c2368ce8..db31243ea 100644 --- a/libs/environment/src/hooks/use-config.tsx +++ b/libs/environment/src/hooks/use-config.tsx @@ -1,27 +1,50 @@ import type { Dispatch, SetStateAction } from 'react'; import { useState, useEffect } from 'react'; import { LocalStorage } from '@vegaprotocol/react-helpers'; +import { ErrorType } from '../types'; import type { Environment, Configuration, - ConfigStatus, Networks, } from '../types'; import { validateConfiguration } from '../utils/validate-configuration'; import { promiseRaceToSuccess } from '../utils/promise-race-success'; +import { requestNode } from '../utils/request-node'; +import { getHasInvalidChain } from '../utils/validate-node'; export const LOCAL_STORAGE_NETWORK_KEY = 'vegaNetworkConfig'; export type EnvironmentWithOptionalUrl = Partial & Omit; -const requestToNode = async (url: string, index: number): Promise => { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed connecting to node: ${url}.`); +const requestToNode = (env: Networks, setSubscriptionStatus: (url: string, status: boolean) => void) => async (url: string): Promise => new Promise((resolve, reject) => { + requestNode(url, { + onStatsSuccess: (data) => { + if (getHasInvalidChain(env, data.statistics.chainId)) { + reject(ErrorType.INVALID_NETWORK); + return; + } + resolve(url); + return; + }, + onStatsFailure: () => { + reject(ErrorType.CONNECTION_ERROR); + }, + onSubscriptionSuccess: () => { + setSubscriptionStatus(url, true); + }, + onSubscriptionFailure: () => { + setSubscriptionStatus(url, false); + }, + }); +}); + +const compileHosts = (hosts: string[], envUrl?: string) => { + if (envUrl && !hosts.includes(envUrl)) { + return [...hosts, envUrl]; } - return index; -}; + return hosts; +} const getCachedConfig = (env: Networks) => { const value = LocalStorage.getItem(LOCAL_STORAGE_NETWORK_KEY); @@ -39,7 +62,7 @@ const getCachedConfig = (env: Networks) => { } catch (err) { LocalStorage.removeItem(LOCAL_STORAGE_NETWORK_KEY); console.warn( - 'Malformed data found for network configuration. Removed and continuing...' + 'Malformed data found for network configuration. Removed cached configuration, continuing...' ); } } @@ -49,84 +72,100 @@ const getCachedConfig = (env: Networks) => { export const useConfig = ( environment: EnvironmentWithOptionalUrl, - updateEnvironment: Dispatch> + updateEnvironment: Dispatch>, + onNodeError: (errorType: ErrorType) => void, + onConfigLoadError: () => void, ) => { + const [verified, setVerified] = useState(false); + const [subscriptionStatusMap, setSubscriptionStatusMap] = useState>({}); const [config, setConfig] = useState( getCachedConfig(environment.VEGA_ENV) ); - const [status, setStatus] = useState( - !environment.VEGA_URL ? 'idle' : 'success' - ); useEffect(() => { - if (!config && status === 'idle') { + if (!config) { (async () => { - setStatus('loading-config'); try { const response = await fetch(environment.VEGA_CONFIG_URL ?? ''); const configData: Configuration = await response.json(); if (validateConfiguration(configData)) { - setStatus('error-validating-config'); + onConfigLoadError(); return; } - setConfig({ hosts: configData.hosts }); + const hosts = compileHosts(configData.hosts, environment.VEGA_URL); + + setConfig({ hosts }); LocalStorage.setItem( LOCAL_STORAGE_NETWORK_KEY, JSON.stringify({ [environment.VEGA_ENV]: { - hosts: configData.hosts, + hosts, }, }) ); } catch (err) { - setStatus('error-loading-config'); + onConfigLoadError(); } })(); } // load config only once per runtime // eslint-disable-next-line react-hooks/exhaustive-deps - }, [environment.VEGA_CONFIG_URL, !!config, status, setStatus, setConfig]); + }, [environment.VEGA_CONFIG_URL, environment.VEGA_URL, !!config, setConfig, onConfigLoadError]); useEffect(() => { - if ( - config && - !['loading-node', 'success', 'error-loading-node'].includes(status) - ) { - (async () => { - setStatus('loading-node'); - - // if there's only one configured node to choose from, set is as the env url - if (config.hosts.length === 1) { - setStatus('success'); - updateEnvironment((prevEnvironment) => ({ - ...prevEnvironment, - VEGA_URL: config.hosts[0], - })); - return; - } - - // when there are multiple possible hosts, set the env url to the node which responds first + (async () => { + if (environment.VEGA_URL && !verified) { try { - const requests = config.hosts.map(requestToNode); - const index = await promiseRaceToSuccess(requests); - setStatus('success'); + await requestToNode(environment.VEGA_ENV, (index, status) => { + setSubscriptionStatusMap(state => ({ + ...state, + [index]: status, + })) + })(environment.VEGA_URL); + setVerified(true); + } catch (err: any) { + if (err in ErrorType) { + onNodeError(err); + return; + } + onNodeError(ErrorType.CONNECTION_ERROR); + } + } + + if (config && !environment.VEGA_URL) { + try { + const requests = config.hosts.map(requestToNode(environment.VEGA_ENV, (url, status) => { + setSubscriptionStatusMap(state => ({ + ...state, + [url]: status, + })) + })); + + const node = await promiseRaceToSuccess(requests); + + setVerified(true); updateEnvironment((prevEnvironment) => ({ ...prevEnvironment, - VEGA_URL: config.hosts[index], + VEGA_URL: node, })); - } catch (err) { - setStatus('error-loading-node'); + } catch (err: any) { + onNodeError(ErrorType.CONNECTION_ERROR_ALL); } - })(); - } + } + })(); // load config only once per runtime // eslint-disable-next-line react-hooks/exhaustive-deps - }, [status, !!config, setStatus, updateEnvironment]); + }, [environment.VEGA_URL, verified, !!config, updateEnvironment]); + + useEffect(() => { + if (subscriptionStatusMap[environment.VEGA_URL ?? ''] === false) { + onNodeError(ErrorType.SSL_ERROR); + } + }, [onNodeError, subscriptionStatusMap[environment.VEGA_URL ?? '']]) return { - status, config, }; }; diff --git a/libs/environment/src/hooks/use-environment.tsx b/libs/environment/src/hooks/use-environment.tsx index 5112305d4..0080d481f 100644 --- a/libs/environment/src/hooks/use-environment.tsx +++ b/libs/environment/src/hooks/use-environment.tsx @@ -5,7 +5,7 @@ import { NodeSwitcherDialog } from '../components/node-switcher-dialog'; import { useConfig } from './use-config'; import { compileEnvironment } from '../utils/compile-environment'; import { validateEnvironment } from '../utils/validate-environment'; -import type { Environment, RawEnvironment, ConfigStatus } from '../types'; +import type { Environment, RawEnvironment } from '../types'; type EnvironmentProviderProps = { definitions?: Partial; @@ -13,7 +13,6 @@ type EnvironmentProviderProps = { }; export type EnvironmentState = Environment & { - configStatus: ConfigStatus; setNodeSwitcherOpen: () => void; }; @@ -27,9 +26,11 @@ export const EnvironmentProvider = ({ const [environment, updateEnvironment] = useState( compileEnvironment(definitions) ); - const { status: configStatus, config } = useConfig( + const { config } = useConfig( environment, - updateEnvironment + updateEnvironment, + () => setNodeSwitcherOpen(true), + () => setNodeSwitcherOpen(true), ); const errorMessage = validateEnvironment(environment); @@ -46,7 +47,6 @@ export const EnvironmentProvider = ({ setNodeSwitcherOpen(true), }} > diff --git a/libs/environment/src/types.ts b/libs/environment/src/types.ts index cd6ecb9fa..daaba4375 100644 --- a/libs/environment/src/types.ts +++ b/libs/environment/src/types.ts @@ -8,6 +8,14 @@ export { ENV_KEYS, Networks }; export const CUSTOM_NODE_KEY = 'custom'; +export enum ErrorType { + INVALID_URL, + INVALID_NETWORK, + SSL_ERROR, + CONNECTION_ERROR, + CONNECTION_ERROR_ALL, +} + export type Environment = z.infer & { // provide this manually, zod fails to compile the correct type fot VEGA_NETWORKS VEGA_NETWORKS: Partial>; @@ -19,15 +27,6 @@ export type RawEnvironment = Record; export type Configuration = z.infer; -export type ConfigStatus = - | 'idle' - | 'success' - | 'loading-config' - | 'loading-node' - | 'error-loading-config' - | 'error-validating-config' - | 'error-loading-node'; - type NodeCheck = { isLoading: boolean; hasError: boolean; diff --git a/libs/environment/src/utils/initialize-node.tsx b/libs/environment/src/utils/initialize-node.tsx index d2fef1247..53adfc59a 100644 --- a/libs/environment/src/utils/initialize-node.tsx +++ b/libs/environment/src/utils/initialize-node.tsx @@ -1,26 +1,7 @@ import type { Dispatch } from 'react'; -import { gql } from '@apollo/client'; -import createClient from './apollo-client'; import { ACTIONS } from '../hooks/use-nodes'; import type { Action } from '../hooks/use-nodes'; -import type { Statistics } from './__generated__/Statistics'; - -export const STATS_QUERY = gql` - query Statistics { - statistics { - chainId - blockHeight - } - } -`; - -export const TIME_UPDATE_SUBSCRIPTION = gql` - subscription BlockTime { - busEvents(types: TimeUpdate, batchSize: 1) { - eventId - } - } -`; +import { requestNode } from './request-node'; const getResponseTime = (url: string) => { const requests = window.performance.getEntriesByName(url); @@ -33,69 +14,42 @@ export const initializeNode = ( node: string, nodeUrl?: string ) => { + let isMounted = true; const url = nodeUrl ?? node; dispatch({ type: ACTIONS.GET_STATISTICS, node, payload: { url } }); dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION, node, payload: { url } }); - try { - new URL(url); - } catch (err) { - dispatch({ type: ACTIONS.GET_STATISTICS_FAILURE, node }); - dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_FAILURE, node }); - return { - client: undefined, - unsubscribe: () => { - isMounted = false; - }, - }; - } - - const client = createClient(url); - let isMounted = true; - - client - .query({ - query: STATS_QUERY, - }) - .then((res) => { + const client = requestNode(url, { + onStatsSuccess: data => { isMounted && dispatch({ type: ACTIONS.GET_STATISTICS_SUCCESS, node, payload: { - chain: res.data.statistics.chainId, - block: Number(res.data.statistics.blockHeight), + chain: data.statistics.chainId, + block: Number(data.statistics.blockHeight), responseTime: getResponseTime(url), }, }); - }) - .catch(() => { + }, + onStatsFailure: () => { isMounted && dispatch({ type: ACTIONS.GET_STATISTICS_FAILURE, node }); - }); - - const subscription = client - .subscribe({ - query: TIME_UPDATE_SUBSCRIPTION, - errorPolicy: 'all', - }) - .subscribe({ - next() { - isMounted && - dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_SUCCESS, node }); - subscription.unsubscribe(); - }, - error() { - isMounted && - dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_FAILURE, node }); - subscription.unsubscribe(); - }, - }); + }, + onSubscriptionSuccess: () => { + isMounted && + dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_SUCCESS, node }); + }, + onSubscriptionFailure: () => { + isMounted && + dispatch({ type: ACTIONS.CHECK_SUBSCRIPTION_FAILURE, node }); + }, + }); return { client, unsubscribe: () => { - client.stop(); + client?.stop(); isMounted = false; }, }; diff --git a/libs/environment/src/utils/request-node.ts b/libs/environment/src/utils/request-node.ts new file mode 100644 index 000000000..89837a02f --- /dev/null +++ b/libs/environment/src/utils/request-node.ts @@ -0,0 +1,76 @@ +import { gql } from '@apollo/client'; +import createClient from './apollo-client'; +import type { Statistics } from './__generated__/Statistics'; + +export const STATS_QUERY = gql` + query Statistics { + statistics { + chainId + blockHeight + } + } +`; + +export const TIME_UPDATE_SUBSCRIPTION = gql` + subscription BlockTime { + busEvents(types: TimeUpdate, batchSize: 1) { + eventId + } + } +`; + +type Callbacks = { + onStatsSuccess: (data: Statistics) => void, + onStatsFailure: () => void, + onSubscriptionSuccess: () => void, + onSubscriptionFailure: () => void, +} + +export const requestNode = ( + url: string, + { + onStatsSuccess, + onStatsFailure, + onSubscriptionSuccess, + onSubscriptionFailure, + }: Callbacks +) => { + try { + new URL(url); + } catch (err) { + onStatsFailure(); + onSubscriptionFailure(); + return; + } + + const client = createClient(url); + + client + .query({ + query: STATS_QUERY, + }) + .then((res) => { + onStatsSuccess(res.data); + }) + .catch(() => { + onStatsFailure(); + }); + + const subscription = client + .subscribe({ + query: TIME_UPDATE_SUBSCRIPTION, + errorPolicy: 'all', + }) + .subscribe({ + next() { + onSubscriptionSuccess(); + subscription.unsubscribe(); + }, + error() { + onSubscriptionFailure(); + subscription.unsubscribe(); + }, + }); + + return client; +} diff --git a/libs/environment/src/utils/validate-node.tsx b/libs/environment/src/utils/validate-node.tsx index 16e27fd22..4fd576799 100644 --- a/libs/environment/src/utils/validate-node.tsx +++ b/libs/environment/src/utils/validate-node.tsx @@ -1,5 +1,5 @@ import { t } from '@vegaprotocol/react-helpers'; -import { CUSTOM_NODE_KEY } from '../types'; +import { CUSTOM_NODE_KEY, ErrorType } from '../types'; import type { Networks, NodeData } from '../types'; export const getIsNodeLoading = ({ @@ -16,7 +16,7 @@ export const getIsNodeLoading = ({ ); }; -const getHasInvalidChain = (env: Networks, chain?: string) => { +export const getHasInvalidChain = (env: Networks, chain?: string) => { return !(chain?.includes(env.toLowerCase()) ?? false); }; @@ -62,20 +62,42 @@ export const getIsFormDisabled = ( return getIsNodeDisabled(env, data); }; -export const getErrorMessage = (env: Networks, data?: NodeData) => { +export const getErrorByType = (errorType: ErrorType | undefined, env: Networks, url?: string) => { + switch (errorType) { + case ErrorType.INVALID_URL: return { + headline: t('Error: invalid url'), + message: t(url ? `${url} is not a valid url.` : ''), + } + case ErrorType.INVALID_NETWORK: return { + headline: t(`Error: incorrect network`), + message: t(`This node is not on the ${env} network.`), + } + case ErrorType.SSL_ERROR: return { + headline: t(`Error: the node you are reading from does not have SSL`), + message: t( + '${data.url} does not have SSL. SSL is required to subscribe to data.' + ), + } + case ErrorType.CONNECTION_ERROR: return { + headline: t(`Error: can't connect to node`), + message: t(url ? `There was an error connecting to ${url}.` : ''), + } + case ErrorType.CONNECTION_ERROR_ALL: return { + headline: t(`Error: can't connect to any of the nodes on the network`), + message: t(`Please try entering a custom node address, or try again later.`), + } + default: return null; + } +} + +export const getErrorByData = (env: Networks, data?: NodeData) => { if (data && !getIsNodeLoading(data)) { if (getHasInvalidChain(env, data.chain.value)) { - return { - headline: t(`Error: incorrect network`), - message: t(`This node is not on the ${env} network.`), - }; + return getErrorByType(ErrorType.INVALID_NETWORK, env, data.url); } if (getHasInvalidUrl(data.url)) { - return { - headline: t('Error: invalid url'), - message: t(`${data.url} is not a valid url.`), - }; + return getErrorByType(ErrorType.INVALID_URL, env, data.url); } if ( @@ -83,24 +105,13 @@ export const getErrorMessage = (env: Networks, data?: NodeData) => { data.responseTime.hasError || data.block.hasError ) { - return { - headline: t(`Error: can't connect to node`), - message: t(`There was an error connecting to ${data.url}.`), - }; + return getErrorByType(ErrorType.CONNECTION_ERROR, env, data.url); } if (data.ssl.hasError) { - return { - headline: t(`Error: the node you are reading from does not have SSL`), - message: t( - '${data.url} does not have SSL. SSL is required to subscribe to data.' - ), - }; + return getErrorByType(ErrorType.SSL_ERROR, env, data.url); } } - return { - message: undefined, - headline: undefined, - }; + return null; };