feat: wip refactor config hook to run on init
This commit is contained in:
@@ -1 +0,0 @@
|
||||
export * from './network-loader';
|
||||
@@ -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<T>({
|
||||
}: NetworkLoaderProps<T>) {
|
||||
// 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) {
|
||||
|
||||
@@ -9,6 +9,7 @@ type NodeSwitcherDialogProps = ComponentProps<typeof NodeSwitcher> & {
|
||||
|
||||
export const NodeSwitcherDialog = ({
|
||||
config,
|
||||
initialErrorType,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
onConnect,
|
||||
@@ -17,6 +18,7 @@ export const NodeSwitcherDialog = ({
|
||||
<Dialog open={dialogOpen} contentClassNames="md:w-[856px] w-[856px]">
|
||||
<NodeSwitcher
|
||||
config={config}
|
||||
initialErrorType={initialErrorType}
|
||||
onConnect={(url) => {
|
||||
onConnect(url);
|
||||
setDialogOpen(false);
|
||||
|
||||
@@ -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<string, NodeData>) => {
|
||||
}, 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 (
|
||||
<div className="text-black dark:text-white min-w-[800px]">
|
||||
<NodeError {...currentNodeError} />
|
||||
<NodeError {...(currentNodeError || networkError)} />
|
||||
<form onSubmit={() => onSubmit(nodeRadio)}>
|
||||
<p className="text-body-large font-bold mt-16 mb-32">
|
||||
{t('Select a GraphQL node to connect to:')}
|
||||
@@ -85,7 +88,10 @@ export const NodeSwitcher = ({ config, onConnect }: NodeSwitcherProps) => {
|
||||
<RadioGroup
|
||||
className="block"
|
||||
value={nodeRadio}
|
||||
onChange={(value) => setNodeRadio(value)}
|
||||
onChange={(value) => {
|
||||
setNodeRadio(value)
|
||||
setNetworkError(null);
|
||||
}}
|
||||
>
|
||||
<div className="w-full">
|
||||
{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)}
|
||||
/>
|
||||
<Link onClick={() => setCustomNode(customNodeText)}>
|
||||
<Link onClick={() => {
|
||||
setNetworkError(null);
|
||||
setCustomNode(customNodeText)
|
||||
}}>
|
||||
{getIsNodeLoading(state[CUSTOM_NODE_KEY])
|
||||
? t('Checking')
|
||||
: t('Check')}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Environment> &
|
||||
Omit<Environment, 'VEGA_URL'>;
|
||||
|
||||
const requestToNode = async (url: string, index: number): Promise<number> => {
|
||||
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<string> => 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<SetStateAction<Environment>>
|
||||
updateEnvironment: Dispatch<SetStateAction<Environment>>,
|
||||
onNodeError: (errorType: ErrorType) => void,
|
||||
onConfigLoadError: () => void,
|
||||
) => {
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [subscriptionStatusMap, setSubscriptionStatusMap] = useState<Record<string, boolean>>({});
|
||||
const [config, setConfig] = useState<Configuration | undefined>(
|
||||
getCachedConfig(environment.VEGA_ENV)
|
||||
);
|
||||
const [status, setStatus] = useState<ConfigStatus>(
|
||||
!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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<RawEnvironment>;
|
||||
@@ -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<Environment>(
|
||||
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 = ({
|
||||
<EnvironmentContext.Provider
|
||||
value={{
|
||||
...environment,
|
||||
configStatus,
|
||||
setNodeSwitcherOpen: () => setNodeSwitcherOpen(true),
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<typeof envSchema> & {
|
||||
// provide this manually, zod fails to compile the correct type fot VEGA_NETWORKS
|
||||
VEGA_NETWORKS: Partial<Record<Networks, string>>;
|
||||
@@ -19,15 +27,6 @@ export type RawEnvironment = Record<EnvKey, string>;
|
||||
|
||||
export type Configuration = z.infer<typeof configSchema>;
|
||||
|
||||
export type ConfigStatus =
|
||||
| 'idle'
|
||||
| 'success'
|
||||
| 'loading-config'
|
||||
| 'loading-node'
|
||||
| 'error-loading-config'
|
||||
| 'error-validating-config'
|
||||
| 'error-loading-node';
|
||||
|
||||
type NodeCheck<T> = {
|
||||
isLoading: boolean;
|
||||
hasError: boolean;
|
||||
|
||||
@@ -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<Statistics>({
|
||||
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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<Statistics>({
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user