chore: add use throttled data provider hook (#1773)

* chore: add useThrottledDataProvider hook

* chore: add useThrottledDataProvider hook tests
This commit is contained in:
Bartłomiej Głownia 2022-10-24 20:34:16 +02:00 committed by GitHub
parent d0976bbd46
commit 630ef4a22d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 139 additions and 226 deletions

View File

@ -59,11 +59,6 @@ let mockOrderBookData = {
data: mockData,
};
jest.mock('@vegaprotocol/market-depth', () => ({
...jest.requireActual('@vegaprotocol/market-depth'),
useOrderBookData: jest.fn(() => mockOrderBookData),
}));
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn(() => ({
@ -71,6 +66,7 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
marketsConnection: [],
},
})),
useThrottledDataProvider: jest.fn(() => mockOrderBookData),
}));
describe('useCalculateSlippage Hook', () => {

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { Side } from '@vegaprotocol/types';
import { useOrderBookData } from '@vegaprotocol/market-depth';
import { marketDepthProvider } from '@vegaprotocol/market-depth';
import { marketProvider } from '@vegaprotocol/market-list';
import type { SingleMarketFieldsFragment } from '@vegaprotocol/market-list';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
@ -9,6 +9,7 @@ import {
formatNumber,
toBigNum,
useDataProvider,
useThrottledDataProvider,
} from '@vegaprotocol/react-helpers';
interface Props {
@ -18,10 +19,13 @@ interface Props {
export const useCalculateSlippage = ({ marketId, order }: Props) => {
const variables = useMemo(() => ({ marketId }), [marketId]);
const { data } = useOrderBookData({
variables,
throttleMilliseconds: 5000,
});
const { data } = useThrottledDataProvider(
{
dataProvider: marketDepthProvider,
variables,
},
5000
);
const { data: market } = useDataProvider<SingleMarketFieldsFragment, never>({
dataProvider: marketProvider,
noUpdate: true,

View File

@ -7,4 +7,3 @@ export * from './orderbook-manager';
export * from './orderbook-row';
export * from './orderbook.stories';
export * from './orderbook';
export * from './use-orderbook-data';

View File

@ -1,129 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { renderHook, act } from '@testing-library/react';
import { MarketTradingMode } from '@vegaprotocol/types';
import type { MarketDepth_market, MarketDepth_market_data } from './';
import { useOrderBookData } from './use-orderbook-data';
const mockData: MarketDepth_market = {
__typename: 'Market',
id: 'marketId',
decimalPlaces: 5,
positionDecimalPlaces: 0,
data: {
__typename: 'MarketData',
staticMidPrice: '7820',
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
indicativeVolume: '0',
indicativePrice: '0',
bestStaticBidPrice: '7820',
bestStaticOfferPrice: '7821',
market: {
__typename: 'Market',
id: 'marketId',
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: { __typename: 'Trade', price: '7846' },
sell: [
{
__typename: 'PriceLevel',
price: '7861',
volume: '25631',
numberOfOrders: '4',
},
],
buy: [
{
__typename: 'PriceLevel',
price: '7820',
volume: '28',
numberOfOrders: '1',
},
],
sequenceNumber: '1661857812317962664',
},
};
let updateMock: ({ data }: { data: MarketDepth_market }) => boolean;
const mockUseDataProvider = ({ update }: { update: () => boolean }) => {
updateMock = update;
return { data: mockData, loading: false, error: false };
};
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn((args) => mockUseDataProvider(args)),
}));
const modMock = (staticMidPrice: string): MarketDepth_market => {
return {
...mockData,
data: {
...mockData.data,
staticMidPrice,
} as MarketDepth_market_data,
};
};
describe('useOrderBookData hook', () => {
it('should return proper data', () => {
const { result } = renderHook(
() => useOrderBookData({ variables: { marketId: 'marketId' } }),
{
wrapper: MockedProvider,
}
);
expect(result.current.data).toEqual(mockData);
});
it('should update data object', () => {
const { result } = renderHook(
() => useOrderBookData({ variables: { marketId: 'marketId' } }),
{
wrapper: MockedProvider,
}
);
expect(result.current.data?.data?.staticMidPrice).toEqual('7820');
const updateMockData = modMock('1111');
act(() => {
updateMock({ data: updateMockData });
});
expect(result.current.data?.data?.staticMidPrice).toEqual('1111');
});
it('throttling should delay update', async () => {
const { result } = renderHook(
() =>
useOrderBookData({
variables: { marketId: 'marketId' },
throttleMilliseconds: 500,
}),
{
wrapper: MockedProvider,
}
);
expect(result.current.data?.data?.staticMidPrice).toEqual('7820');
const updateMockData = modMock('2222');
const updateMockData2 = modMock('3333');
await act(async () => {
updateMock({ data: updateMockData });
updateMock({ data: updateMockData2 });
});
expect(result.current.data?.data?.staticMidPrice).toEqual('2222');
await new Promise((res) => {
setTimeout(res, 400);
});
expect(result.current.data?.data?.staticMidPrice).toEqual('2222');
await new Promise((res) => {
setTimeout(res, 200);
});
expect(result.current.data?.data?.staticMidPrice).toEqual('3333');
});
});

View File

@ -1,68 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import throttle from 'lodash/throttle';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { marketDepthProvider } from './market-depth-provider';
import type { MarketDepthQuery } from './__generated___/MarketDepth';
interface Props {
variables: { marketId: string };
throttleMilliseconds?: number;
}
export const useOrderBookData = ({
variables,
throttleMilliseconds = 1000,
}: Props) => {
const [orderbookData, setOrderbookData] = useState<
MarketDepthQuery['market'] | null
>(null);
const dataRef = useRef<MarketDepthQuery['market'] | null>(null);
const updateOrderbookData = useRef(
throttle(() => {
if (!dataRef.current) {
return;
}
setOrderbookData(dataRef.current);
}, throttleMilliseconds)
);
const update = useCallback(
({ data }: { data: MarketDepthQuery['market'] | null }) => {
if (!data) {
return false;
}
dataRef.current = data;
updateOrderbookData.current();
return true;
},
[]
);
const { data, error, loading } = useDataProvider({
dataProvider: marketDepthProvider,
update,
variables,
});
useEffect(() => {
const throttleRunnner = updateOrderbookData.current;
if (!data) {
dataRef.current = null;
setOrderbookData(dataRef.current);
return;
}
dataRef.current = {
...data,
};
setOrderbookData(dataRef.current);
return () => {
throttleRunnner.cancel();
};
}, [data]);
return {
loading,
error,
data: orderbookData,
};
};

View File

@ -0,0 +1,68 @@
import { renderHook, act } from '@testing-library/react';
import { useThrottledDataProvider } from './use-data-provider';
import type { Subscribe, UpdateCallback } from '../lib/generic-data-provider';
import { MockedProvider } from '@apollo/client/testing';
type Data = number;
type Delta = number;
const unsubscribe = jest.fn();
const reload = jest.fn();
const flush = jest.fn();
const load = jest.fn();
const updateCallbackPayload: Parameters<UpdateCallback<Data, Delta>>['0'] = {
data: 0,
loading: false,
loaded: false,
pageInfo: null,
};
const dataProvider = jest.fn<
ReturnType<Subscribe<Data, Delta>>,
Parameters<Subscribe<Data, Delta>>
>();
dataProvider.mockReturnValue({
unsubscribe,
reload,
flush,
load,
});
jest.useFakeTimers();
describe('useThrottledDataProvider hook', () => {
it('throttling should delay update', async () => {
const wait = 100;
const { result } = renderHook(
() =>
useThrottledDataProvider(
{
dataProvider,
},
wait
),
{ wrapper: MockedProvider }
);
expect(result.current.data).toEqual(null);
expect(result.current.loading).toEqual(true);
expect(result.current.error).toEqual(undefined);
const callback =
dataProvider.mock.calls[dataProvider.mock.calls.length - 1][0];
await act(async () => {
callback({ ...updateCallbackPayload, data: 1 }); // initial value
});
await act(async () => {
callback({ ...updateCallbackPayload, data: 2, isUpdate: true, delta: 1 }); // first update, executed immediately
callback({ ...updateCallbackPayload, data: 3, isUpdate: true, delta: 1 }); // next update, should be excluded
callback({ ...updateCallbackPayload, data: 4, isUpdate: true, delta: 1 }); // next update, should be excluded
callback({ ...updateCallbackPayload, data: 5, isUpdate: true, delta: 1 }); // last update, should be executed after timeout
});
expect(result.current.data).toEqual(2);
await act(async () => {
jest.runAllTimers();
});
expect(result.current.data).toEqual(5);
});
});

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import throttle from 'lodash/throttle';
import { useApolloClient } from '@apollo/client';
import type { OperationVariables } from '@apollo/client';
import type {
@ -14,22 +15,7 @@ function hasDelta<T>(
return !!updateData.isUpdate;
}
/**
*
* @param dataProvider subscribe function created by makeDataProvider
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
* @param variables optional
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/
export function useDataProvider<Data, Delta>({
dataProvider,
update,
insert,
variables,
updateOnInit,
noUpdate,
skip,
}: {
interface useDataProviderParams<Data, Delta> {
dataProvider: Subscribe<Data, Delta>;
update?: ({ delta, data }: { delta?: Delta; data: Data }) => boolean;
insert?: ({
@ -45,7 +31,24 @@ export function useDataProvider<Data, Delta>({
updateOnInit?: boolean;
noUpdate?: boolean;
skip?: boolean;
}) {
}
/**
*
* @param dataProvider subscribe function created by makeDataProvider
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
* @param variables optional
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/
export const useDataProvider = <Data, Delta>({
dataProvider,
update,
insert,
variables,
updateOnInit,
noUpdate,
skip,
}: useDataProviderParams<Data, Delta>) => {
const client = useApolloClient();
const [data, setData] = useState<Data | null>(null);
const [totalCount, setTotalCount] = useState<number>();
@ -131,4 +134,44 @@ export function useDataProvider<Data, Delta>({
return unsubscribe;
}, [client, initialized, dataProvider, callback, variables, skip]);
return { data, loading, error, flush, reload, load, totalCount };
}
};
export const useThrottledDataProvider = <Data, Delta>(
params: Omit<useDataProviderParams<Data, Delta>, 'update'>,
wait?: number
) => {
const [data, setData] = useState<Data | null>(null);
const dataRef = useRef<Data | null>(null);
const updateData = useRef(
throttle(() => {
if (!dataRef.current) {
return;
}
setData(dataRef.current);
}, wait)
);
const update = useCallback(({ data }: { data: Data | null }) => {
if (!data) {
return false;
}
dataRef.current = data;
updateData.current();
return true;
}, []);
const returnValues = useDataProvider({ ...params, update });
useEffect(() => {
const throttledUpdate = updateData.current;
return () => {
throttledUpdate.cancel();
};
}, []);
useEffect(() => {
setData(returnValues.data);
}, [returnValues.data]);
return { ...returnValues, data };
};