Compare commits

..
Author SHA1 Message Date
asiaznik bb98dcfae8 fix(environment): pick best node
chore: added response time comparison to the node picker
2024-02-06 14:16:59 +01:00
m.ray ecd2865bd0 fix(trading): disable chart settings temporarily in default env (#5750) 2024-02-05 17:03:34 +00:00
m.ray adf0e26df6 chore(trading): clear session storage and update readme.md in main (#5747) 2024-02-05 16:23:03 +00:00
m.ray a7f95aa953 chore(trading): disable trading view temporarily (#5742) 2024-02-05 11:27:20 +00:00
10 changed files with 153 additions and 68 deletions
+5 -5
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev).
# 🔎 Applications in this repo
## 🔎 Applications in this repo
### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
# 🧱 Libraries in this repo
## 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities.
# 💻 Develop
## 💻 Develop
### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console
## 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
```
# 📑 License
## 📑 License
[MIT](./LICENSE)
+2 -2
View File
@@ -29,5 +29,5 @@ NX_REFERRALS=true
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
# NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
# NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
@@ -81,6 +81,7 @@ export const Settings = () => {
intent={Intent.Primary}
onClick={() => {
localStorage.clear();
sessionStorage.clear();
window.location.reload();
}}
>
@@ -23,7 +23,6 @@ import {
SUBSCRIPTION_TIMEOUT,
useNodeBasicStatus,
useNodeSubscriptionStatus,
useResponseTime,
} from './row-data';
import { BLOCK_THRESHOLD, RowData } from './row-data';
import { CUSTOM_NODE_KEY } from '../../types';
@@ -162,19 +161,6 @@ describe('useNodeBasicStatus', () => {
});
});
describe('useResponseTime', () => {
it('returns response time when url is valid', () => {
const { result } = renderHook(() =>
useResponseTime('https://localhost:1234')
);
expect(result.current.responseTime).toBe(50);
});
it('does not return response time when url is invalid', () => {
const { result } = renderHook(() => useResponseTime('nope'));
expect(result.current.responseTime).toBeUndefined();
});
});
describe('RowData', () => {
const props = {
id: '0',
@@ -1,4 +1,3 @@
import { isValidUrl } from '@vegaprotocol/utils';
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react';
import { CUSTOM_NODE_KEY } from '../../types';
@@ -8,6 +7,7 @@ import {
} from '../../utils/__generated__/NodeCheck';
import { LayoutCell } from './layout-cell';
import { useT } from '../../use-t';
import { useResponseTime } from '../../utils/time';
export const POLL_INTERVAL = 1000;
export const SUBSCRIPTION_TIMEOUT = 3000;
@@ -108,20 +108,6 @@ export const useNodeBasicStatus = () => {
};
};
export const useResponseTime = (url: string, trigger?: unknown) => {
const [responseTime, setResponseTime] = useState<number>();
useEffect(() => {
if (!isValidUrl(url)) return;
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
const { duration } =
(requests.length && requests[requests.length - 1]) || {};
setResponseTime(duration);
}, [url, trigger]);
return { responseTime };
};
export const RowData = ({
id,
url,
@@ -10,6 +10,7 @@ import {
getUserEnabledFeatureFlags,
setUserEnabledFeatureFlag,
} from './use-environment';
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
const noop = () => {
/* no op*/
@@ -17,6 +18,10 @@ const noop = () => {
jest.mock('@vegaprotocol/apollo-client');
jest.mock('zustand');
jest.mock('../utils/time');
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
const mockCreateClient = createClient as jest.Mock;
const createDefaultMockClient = () => {
@@ -155,6 +160,14 @@ describe('useEnvironment', () => {
const fastNode = 'https://api.n01.foo.vega.xyz';
const fastWait = 1000;
const nodes = [slowNode, fastNode];
mockCanMeasureResponseTime.mockImplementation(() => true);
mockMeasureResponseTime.mockImplementation((url: string) => {
if (url === slowNode) return slowWait;
if (url === fastNode) return fastWait;
return Infinity;
});
// @ts-ignore: typscript doesn't recognise the mock implementation
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
@@ -168,7 +181,7 @@ describe('useEnvironment', () => {
statistics: {
chainId: 'chain-id',
blockHeight: '100',
vegaTime: new Date().toISOString(),
vegaTime: new Date(1).toISOString(),
},
},
});
@@ -196,7 +209,8 @@ describe('useEnvironment', () => {
expect(result.current.nodes).toEqual(nodes);
});
jest.runAllTimers();
jest.advanceTimersByTime(2000);
// jest.runAllTimers();
await waitFor(() => {
expect(result.current.status).toEqual('success');
+78 -29
View File
@@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
import { envSchema } from '../utils/validate-environment';
import { tomlConfigSchema } from '../utils/validate-configuration';
import uniq from 'lodash/uniq';
import orderBy from 'lodash/orderBy';
import first from 'lodash/first';
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
type Client = ReturnType<typeof createClient>;
type ClientCollection = {
@@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
const VERSION = 1;
export const STORAGE_KEY = `vega_url_${VERSION}`;
const QUERY_TIMEOUT = 3000;
const SUBSCRIPTION_TIMEOUT = 3000;
const raceAgainst = (timeout: number): Promise<false> =>
new Promise((resolve) => {
setTimeout(() => {
resolve(false);
}, timeout);
});
/**
* Fetch and validate a vega node configuration
*/
@@ -64,53 +76,88 @@ const fetchConfig = async (url?: string) => {
const findNode = async (clients: ClientCollection): Promise<string | null> => {
const tests = Object.entries(clients).map((args) => testNode(...args));
try {
const url = await Promise.any(tests);
return url;
} catch {
const nodes = await Promise.all(tests);
const responsiveNodes = nodes
.filter(([, q, s]) => q && s)
.map(([url, q]) => {
return {
url,
...q,
};
});
// more recent and faster at the top
const ordered = orderBy(
responsiveNodes,
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
['desc', 'desc', 'asc']
);
const best = first(ordered);
return best ? best.url : null;
} catch (err) {
// All tests rejected, no suitable node found
return null;
}
};
type Maybe<T> = T | false;
type QueryTestResult = {
blockHeight: number;
vegaTime: Date;
responseTime: number;
};
type SubscriptionTestResult = true;
type NodeTestResult = [
/** url */
string,
Maybe<QueryTestResult>,
Maybe<SubscriptionTestResult>
];
/**
* Test a node for suitability for connection
*/
const testNode = async (
url: string,
client: Client
): Promise<string | null> => {
): Promise<NodeTestResult> => {
const results = await Promise.all([
// these promises will only resolve with true/false
testQuery(client),
testQuery(client, url),
testSubscription(client),
]);
if (results[0] && results[1]) {
return url;
}
const message = `Tests failed for node: ${url}`;
console.warn(message);
// throwing here will mean this tests is ignored and a different
// node that hopefully does resolve will fulfill the Promise.any
throw new Error(message);
return [url, ...results];
};
/**
* Run a test query on a client
*/
const testQuery = async (client: Client) => {
try {
const result = await client.query<NodeCheckQuery>({
query: NodeCheckDocument,
});
if (!result || result.error) {
return false;
}
return true;
} catch (err) {
return false;
}
const testQuery = (
client: Client,
url: string
): Promise<Maybe<QueryTestResult>> => {
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
client
.query<NodeCheckQuery>({
query: NodeCheckDocument,
})
.then((result) => {
if (result && !result.error) {
const res = {
blockHeight: Number(result.data.statistics.blockHeight),
vegaTime: new Date(result.data.statistics.vegaTime),
// only after a request has been sent we can retrieve the response time
responseTime: canMeasureResponseTime(url)
? measureResponseTime(url) || Infinity
: Infinity,
} as QueryTestResult;
resolve(res);
} else {
resolve(false);
}
})
.catch(() => resolve(false))
);
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
};
/**
@@ -118,7 +165,9 @@ const testQuery = async (client: Client) => {
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
* is deemed a failure
*/
const testSubscription = (client: Client) => {
const testSubscription = (
client: Client
): Promise<Maybe<SubscriptionTestResult>> => {
return new Promise((resolve) => {
const sub = client
.subscribe<NodeCheckTimeUpdateSubscription>({
+22
View File
@@ -0,0 +1,22 @@
import { renderHook } from '@testing-library/react';
import { useResponseTime } from './time';
const mockResponseTime = 50;
global.performance.getEntriesByName = jest.fn().mockReturnValue([
{
duration: mockResponseTime,
},
]);
describe('useResponseTime', () => {
it('returns response time when url is valid', () => {
const { result } = renderHook(() =>
useResponseTime('https://localhost:1234')
);
expect(result.current.responseTime).toBe(50);
});
it('does not return response time when url is invalid', () => {
const { result } = renderHook(() => useResponseTime('nope'));
expect(result.current.responseTime).toBeUndefined();
});
});
+25
View File
@@ -0,0 +1,25 @@
import { isValidUrl } from '@vegaprotocol/utils';
import { useEffect, useState } from 'react';
export const useResponseTime = (url: string, trigger?: unknown) => {
const [responseTime, setResponseTime] = useState<number>();
useEffect(() => {
if (!canMeasureResponseTime(url)) return;
const duration = measureResponseTime(url);
setResponseTime(duration);
}, [url, trigger]);
return { responseTime };
};
export const canMeasureResponseTime = (url: string) => {
if (!isValidUrl(url)) return false;
if (typeof window.performance.getEntriesByName !== 'function') return false;
return true;
};
export const measureResponseTime = (url: string) => {
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
const { duration } = (requests.length && requests[requests.length - 1]) || {};
return duration;
};
+3 -1
View File
@@ -11,7 +11,9 @@
"echo $NX_VEGA_URL",
"echo $NX_TENDERMINT_URL",
"echo $NX_TENDERMINT_WEBSOCKET_URL",
"echo $NX_ETHEREUM_PROVIDER_URL"
"echo $NX_ETHEREUM_PROVIDER_URL",
"echo $NX_CHARTING_LIBRARY_PATH",
"echo $NX_CHARTING_LIBRARY_HASH"
]
}
}