[#128] Add variales handling in data provider

This commit is contained in:
Bartłomiej Głownia 2022-03-27 11:40:27 +02:00
parent aec5d54820
commit e3a1142579
8 changed files with 179 additions and 118 deletions

View File

@ -1,7 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useRef, useCallback } from 'react';
import { produce } from 'immer';
import merge from 'lodash/merge';
import { useApolloClient } from '@apollo/client';
import { useRouter } from 'next/router';
import { AsyncRenderer } from '../../components/async-renderer';
import { MarketListTable, getRowNodeId } from '@vegaprotocol/market-list';
@ -10,60 +9,51 @@ import {
Markets_markets_data,
marketsDataProvider,
} from '@vegaprotocol/graphql';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import type { AgGridReact } from 'ag-grid-react';
const Markets = () => {
const { pathname, push } = useRouter();
const [markets, setMarkets] = useState<Markets_markets[] | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error>();
const client = useApolloClient();
const gridRef = useRef<AgGridReact | null>(null);
const initialized = useRef<boolean>(false);
useEffect(() => {
return marketsDataProvider(client, ({ data, error, loading, delta }) => {
setError(error);
setLoading(loading);
if (!error && !loading) {
if (!initialized.current || !gridRef.current) {
initialized.current = true;
setMarkets(data);
} else {
const update: Markets_markets[] = [];
const add: Markets_markets[] = [];
// split into updates and adds
if (!gridRef.current) return;
const rowNode = gridRef.current.api.getRowNode(
getRowNodeId(delta.market)
);
if (rowNode) {
const updatedData = produce(
rowNode.data.data,
(draft: Markets_markets_data) => merge(draft, delta)
);
if (updatedData !== rowNode.data.data) {
update.push({ ...rowNode.data, data: delta });
}
}
// @TODO - else add new market
if (update.length || add.length) {
gridRef.current.api.applyTransactionAsync({
update,
add,
addIndex: 0,
});
}
const gridRef = useRef<AgGridReact>();
const update = useCallback(
(delta: Markets_markets_data) => {
const update: Markets_markets[] = [];
const add: Markets_markets[] = [];
if (!gridRef.current) {
return false;
}
const rowNode = gridRef.current.api.getRowNode(
getRowNodeId(delta.market)
);
if (rowNode) {
const updatedData = produce<Markets_markets_data>(
rowNode.data.data,
(draft: Markets_markets_data) => merge(draft, delta)
);
if (updatedData !== rowNode.data.data) {
update.push({ ...rowNode.data, data: updatedData });
}
}
});
}, [client, initialized]);
// @TODO - else add new market
if (update.length || add.length) {
gridRef.current.api.applyTransactionAsync({
update,
add,
addIndex: 0,
});
}
return true;
},
[gridRef]
);
const { data, error, loading } = useDataProvider<
Markets_markets,
Markets_markets_data
>(marketsDataProvider, update);
return (
<AsyncRenderer loading={loading} error={error} data={markets}>
<AsyncRenderer loading={loading} error={error} data={data}>
{(data) => (
<MarketListTable
ref={gridRef}

View File

@ -1,4 +1,4 @@
import { useRef, useCallback } from 'react';
import { useRef, useCallback, useMemo } from 'react';
import { produce } from 'immer';
import assign from 'assign-deep';
import { useRouter } from 'next/router';
@ -10,12 +10,15 @@ import {
positionSubscribe_positions,
positionsDataProvider,
} from '@vegaprotocol/graphql';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { AgGridReact } from 'ag-grid-react';
export const Positions = () => {
const { pathname, push } = useRouter();
const gridRef = useRef<AgGridReact>();
const { keypair } = useVegaWallet();
const variables = useMemo(() => ({ partyId: keypair.pub }), [keypair]);
const update = useCallback(
(delta: positionSubscribe_positions) => {
const update: positions_party_positions[] = [];
@ -25,12 +28,14 @@ export const Positions = () => {
}
const rowNode = gridRef.current.api.getRowNode(getRowNodeId(delta));
if (rowNode) {
const updatedData = produce(
const updatedData = produce<positions_party_positions>(
rowNode.data,
(draft: positions_party_positions) => assign(draft, delta)
(draft: positions_party_positions) => {
assign(draft, delta);
}
);
if (updatedData !== rowNode.data) {
update.push(delta);
update.push(updatedData);
}
} else {
add.push(delta);
@ -49,7 +54,7 @@ export const Positions = () => {
const { data, error, loading } = useDataProvider<
positions_party_positions,
positionSubscribe_positions
>(positionsDataProvider, update);
>(positionsDataProvider, update, variables);
return (
<AsyncRenderer loading={loading} error={error} data={data}>
{(data) => (

View File

@ -4,32 +4,60 @@ import type {
ApolloClient,
DocumentNode,
TypedDocumentNode,
OperationVariables,
} from '@apollo/client';
import type { Subscription } from 'zen-observable-ts';
import isEqual from 'lodash/isEqual';
export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
query: DocumentNode | TypedDocumentNode<QueryData, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
subscriptionQuery: DocumentNode | TypedDocumentNode<SubscriptionData, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
update: (draft: Draft<Data>[], delta: Delta) => void,
getData: (subscriptionData: QueryData) => Data[] | null,
getDelta: (subscriptionData: SubscriptionData) => Delta
) {
type C = (arg: {
export interface UpdateCallback<Data, Delta> {
(arg: {
data: Data[] | null;
error?: Error;
loading: boolean;
delta?: Delta;
}) => void;
const callbacks: C[] = [];
}): void;
}
export interface Subscribe<Data, Delta> {
(
callback: UpdateCallback<Data, Delta>,
client: ApolloClient<object>,
variables?: OperationVariables
): void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
interface Update<Data, Delta> {
(draft: Draft<Data>[], delta: Delta): void;
}
interface GetData<QueryData, Data> {
(subscriptionData: QueryData): Data[] | null;
}
interface GetDelta<SubscriptionData, Delta> {
(subscriptionData: SubscriptionData): Delta;
}
function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
query: Query<QueryData>,
subscriptionQuery: Query<SubscriptionData>,
update: Update<Data, Delta>,
getData: GetData<QueryData, Data>,
getDelta: GetDelta<SubscriptionData, Delta>
): Subscribe<Data, Delta> {
const callbacks: UpdateCallback<Data, Delta>[] = [];
const updateQueue: Delta[] = [];
let variables: OperationVariables | undefined = undefined;
let data: Data[] | null = null;
let error: Error | undefined = undefined;
let loading = false;
let client: ApolloClient<object> | undefined = undefined;
let subscription: Subscription | undefined = undefined;
const notify = (callback: C, delta?: Delta) => {
const notify = (callback: UpdateCallback<Data, Delta>, delta?: Delta) => {
callback({
data,
error,
@ -55,6 +83,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
subscription = client
.subscribe<SubscriptionData>({
query: subscriptionQuery,
variables,
})
.subscribe(({ data: subscriptionData }) => {
if (!subscriptionData) {
@ -75,7 +104,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
}
});
try {
const res = await client.query<QueryData>({ query });
const res = await client.query<QueryData>({ query, variables });
data = getData(res.data);
if (data && updateQueue && updateQueue.length > 0) {
data = produce(data, (draft) => {
@ -97,7 +126,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
}
};
const unsubscribe = (callback: C) => {
const unsubscribe = (callback: UpdateCallback<Data, Delta>) => {
callbacks.splice(callbacks.indexOf(callback), 1);
if (callbacks.length === 0) {
if (subscription) {
@ -110,12 +139,11 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
}
};
return (c: ApolloClient<object>, callback: C) => {
if (!client) {
client = c;
}
return (callback, c, v) => {
callbacks.push(callback);
if (callbacks.length === 1) {
client = c;
variables = v;
initialize();
} else {
notify(callback);
@ -123,3 +151,41 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
return () => unsubscribe(callback);
};
}
const memoize = <Data, Delta>(
fn: (variables?: OperationVariables) => Subscribe<Data, Delta>
) => {
const cache: {
subscribe: Subscribe<Data, Delta>;
variables?: OperationVariables;
}[] = [];
return (variables?: OperationVariables) => {
const cached = cache.find((c) => isEqual(c.variables, variables));
if (cached) {
return cached.subscribe;
}
const subscribe = fn(variables);
cache.push({ subscribe, variables });
return subscribe;
};
};
export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
query: Query<QueryData>,
subscriptionQuery: Query<SubscriptionData>,
update: Update<Data, Delta>,
getData: GetData<QueryData, Data>,
getDelta: GetDelta<SubscriptionData, Delta>
): Subscribe<Data, Delta> {
const getInstance = memoize<Data, Delta>((variables) =>
makeDataProviderInternal(
query,
subscriptionQuery,
update,
getData,
getDelta
)
);
return (callback, client, variables) =>
getInstance(variables)(callback, client, variables);
}

View File

@ -1,2 +1,3 @@
export * from './markets-data-provider';
export * from './positions-data-provider';
export type { Subscribe } from './generic-data-provider';

View File

@ -55,22 +55,21 @@ const MARKET_DATA_SUB = gql`
}
`;
const update = (draft: Markets_markets[], delta: MarketDataSub_marketData) => {
const index = draft.findIndex((m) => m.id === delta.market.id);
if (index !== -1) {
draft[index].data = delta;
}
// @TODO - else push new market to draft
};
const getData = (responseData: Markets): Markets_markets[] | null =>
responseData.markets;
const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData =>
subscriptionData.marketData;
export const marketsDataProvider = makeDataProvider<
Markets,
Markets_markets,
MarketDataSub,
MarketDataSub_marketData
>(
MARKETS_QUERY,
MARKET_DATA_SUB,
(draft: Markets_markets[], delta: MarketDataSub_marketData) => {
const index = draft.findIndex((m) => m.id === delta.market.id);
if (index !== -1) {
draft[index].data = delta;
}
// @TODO - else push new market to draft
},
(responseData: Markets): Markets_markets[] | null => responseData.markets,
(subscriptionData: MarketDataSub): MarketDataSub_marketData =>
subscriptionData.marketData
);
>(MARKETS_QUERY, MARKET_DATA_SUB, update, getData, getDelta);

View File

@ -69,24 +69,26 @@ export const POSITIONS_SUB = gql`
}
`;
const update = (
draft: positions_party_positions[],
delta: positionSubscribe_positions
) => {
const index = draft.findIndex((m) => m.market.id === delta.market.id);
if (index !== -1) {
draft[index] = delta;
} else {
draft.push(delta);
}
};
const getData = (responseData: positions): positions_party_positions[] | null =>
responseData.party ? responseData.party.positions : null;
const getDelta = (
subscriptionData: positionSubscribe
): positionSubscribe_positions => subscriptionData.positions;
export const positionsDataProvider = makeDataProvider<
positions,
positions_party_positions,
positionSubscribe,
positionSubscribe_positions
>(
POSITION_QUERY,
POSITIONS_SUB,
(draft: positions_party_positions[], delta: positionSubscribe_positions) => {
const index = draft.findIndex((m) => m.market.id === delta.market.id);
if (index !== -1) {
draft[index] = delta;
} else {
draft.push(delta);
}
},
(responseData: positions): positions_party_positions[] | null =>
responseData.party ? responseData.party.positions : null,
(subscriptionData: positionSubscribe): positionSubscribe_positions =>
subscriptionData.positions
);
>(POSITION_QUERY, POSITIONS_SUB, update, getData, getDelta);

View File

@ -1,26 +1,20 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useApolloClient } from '@apollo/client';
import type { ApolloClient } from '@apollo/client';
import type { OperationVariables } from '@apollo/client';
import type { Subscribe } from '@vegaprotocol/graphql';
export function useDataProvider<Data, Delta>(
dataProvider: (
client: ApolloClient<object>,
callback: (arg: {
data: Data[] | null;
error?: Error;
loading: boolean;
delta?: Delta;
}) => void
) => () => void,
update: (delta: Delta) => boolean = () => false
dataProvider: Subscribe<Data, Delta>,
update: (delta: Delta) => boolean = () => false,
variables?: OperationVariables
) {
const client = useApolloClient();
const [data, setData] = useState<Data[] | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | undefined>(undefined);
const initialized = useRef<boolean>(false);
useEffect(() => {
return dataProvider(client, ({ data, error, loading, delta }) => {
const callback = useCallback(
({ data, error, loading, delta }) => {
setError(error);
setLoading(loading);
if (!error && !loading) {
@ -29,7 +23,11 @@ export function useDataProvider<Data, Delta>(
setData(data);
}
}
});
}, [client, initialized, dataProvider, update]);
},
[update]
);
useEffect(() => {
return dataProvider(callback, client, variables);
}, [client, initialized, dataProvider, callback, variables]);
return { data, loading, error };
}

View File

@ -5,16 +5,16 @@ import * as React from 'react';
import { PriceCell } from './price-cell';
describe('<PriceCell />', () => {
it('Displayes formatted value', () => {
it('Displays formatted value', () => {
render(<PriceCell value={100} valueFormatted="100.00" />);
expect(screen.getByTestId('price')).toHaveTextContent('100.00');
});
it('Displayes 0', () => {
it('Displays 0', () => {
render(<PriceCell value={0} valueFormatted="0.00" />);
expect(screen.getByTestId('price')).toHaveTextContent('0.00');
});
it('Displayes - if value is not a number', () => {
it('Displays - if value is not a number', () => {
render(<PriceCell value={null} valueFormatted="" />);
expect(screen.getByTestId('price')).toHaveTextContent('-');
});