chore(trading): maintain always up to date active orders list (#3430)

This commit is contained in:
Bartłomiej Głownia 2023-04-18 12:49:42 +02:00 committed by GitHub
parent 20507ecdd1
commit 31859cb779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 208 additions and 246 deletions

View File

@ -33,6 +33,8 @@ import { ViewingBanner } from '../components/viewing-banner';
import { Banner } from '../components/banner'; import { Banner } from '../components/banner';
import { AppLoader, DynamicLoader } from '../components/app-loader'; import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar'; import { Navbar } from '../components/navbar';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { activeOrdersProvider } from '@vegaprotocol/orders';
const DEFAULT_TITLE = t('Welcome to Vega trading!'); const DEFAULT_TITLE = t('Welcome to Vega trading!');
@ -95,6 +97,7 @@ function AppBody({ Component }: AppProps) {
<ToastsManager /> <ToastsManager />
<InitializeHandlers /> <InitializeHandlers />
<MaybeConnectEagerly /> <MaybeConnectEagerly />
<PartyData />
</div> </div>
); );
} }
@ -127,6 +130,18 @@ function VegaTradingApp(props: AppProps) {
export default VegaTradingApp; export default VegaTradingApp;
const PartyData = () => {
const { pubKey } = useVegaWallet();
const variables = { partyId: pubKey || '' };
const skip = !pubKey;
useDataProvider({
dataProvider: activeOrdersProvider,
variables,
skip,
});
return null;
};
const MaybeConnectEagerly = () => { const MaybeConnectEagerly = () => {
useVegaEagerConnect(Connectors); useVegaEagerConnect(Connectors);
useEthereumEagerConnect(); useEthereumEagerConnect();

View File

@ -8,7 +8,7 @@ const commonProps = {
describe('DateRangeFilter', () => { describe('DateRangeFilter', () => {
it('should be properly rendered', async () => { it('should be properly rendered', async () => {
const defaultRangeFilter = { const defaultValue = {
start: '2023-02-14T13:53:01+01:00', start: '2023-02-14T13:53:01+01:00',
end: '2023-02-21T13:53:01+01:00', end: '2023-02-21T13:53:01+01:00',
}; };
@ -17,7 +17,7 @@ describe('DateRangeFilter', () => {
render( render(
<DateRangeFilter <DateRangeFilter
{...(commonProps as unknown as DateRangeFilterProps)} {...(commonProps as unknown as DateRangeFilterProps)}
defaultRangeFilter={defaultRangeFilter} defaultValue={defaultValue}
/> />
); );

View File

@ -1,5 +1,5 @@
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community'; import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
@ -17,9 +17,9 @@ import { formatForInput } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { InputError } from '@vegaprotocol/ui-toolkit'; import { InputError } from '@vegaprotocol/ui-toolkit';
const defaultFilterValue: Schema.DateRange = {}; const defaultValue: Schema.DateRange = {};
export interface DateRangeFilterProps extends IFilterParams { export interface DateRangeFilterProps extends IFilterParams {
defaultRangeFilter?: Schema.DateRange; defaultValue?: Schema.DateRange;
maxSubDays?: number; maxSubDays?: number;
maxNextDays?: number; maxNextDays?: number;
maxDaysRange?: number; maxDaysRange?: number;
@ -27,8 +27,9 @@ export interface DateRangeFilterProps extends IFilterParams {
export const DateRangeFilter = forwardRef( export const DateRangeFilter = forwardRef(
(props: DateRangeFilterProps, ref) => { (props: DateRangeFilterProps, ref) => {
const defaultDates = props?.defaultRangeFilter || defaultFilterValue; const defaultDates = props?.defaultValue || defaultValue;
const [value, setValue] = useState<Schema.DateRange>(defaultDates); const [value, setValue] = useState<Schema.DateRange>(defaultDates);
const valueRef = useRef<Schema.DateRange>(value);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => { const [minStartDate, maxStartDate, minEndDate, maxEndDate] = useMemo(() => {
const minStartDate = const minStartDate =
@ -93,7 +94,7 @@ export const DateRangeFilter = forwardRef(
}, },
isFilterActive() { isFilterActive() {
return value.start || value.end; return valueRef.current.start || valueRef.current.end;
}, },
getModel() { getModel() {
@ -101,13 +102,13 @@ export const DateRangeFilter = forwardRef(
return null; return null;
} }
return { value }; return { value: valueRef.current };
}, },
setModel(model?: { value: Schema.DateRange } | null) { setModel(model?: { value: Schema.DateRange } | null) {
setValue( valueRef.current =
model?.value || props?.defaultRangeFilter || defaultFilterValue model?.value || props?.defaultValue || defaultValue;
); setValue(valueRef.current);
}, },
}; };
}); });
@ -185,10 +186,8 @@ export const DateRangeFilter = forwardRef(
update = { ...update, end: checkForEndDate(endDate, startDate) }; update = { ...update, end: checkForEndDate(endDate, startDate) };
if (validate(name, date, update)) { if (validate(name, date, update)) {
setValue((curr) => ({ valueRef.current = { ...valueRef.current, ...update };
...curr, setValue(valueRef.current);
...update,
}));
} }
}; };
useEffect(() => { useEffect(() => {
@ -241,7 +240,8 @@ export const DateRangeFilter = forwardRef(
className="ag-standard-button ag-filter-apply-panel-button" className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => { onClick={() => {
setError(''); setError('');
setValue(defaultDates); valueRef.current = defaultDates;
setValue(valueRef.current);
}} }}
> >
{t('Reset')} {t('Reset')}

View File

@ -1,10 +1,17 @@
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import {
forwardRef,
useEffect,
useImperativeHandle,
useState,
useRef,
} from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community'; import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
export const SetFilter = forwardRef((props: IFilterParams, ref) => { export const SetFilter = forwardRef((props: IFilterParams, ref) => {
const [value, setValue] = useState<string[]>([]); const [value, setValue] = useState<string[]>([]);
const valueRef = useRef(value);
// expose AG Grid Filter Lifecycle callbacks // expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
@ -28,29 +35,28 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
}, },
isFilterActive() { isFilterActive() {
return value.length !== 0; return valueRef.current.length !== 0;
}, },
getModel() { getModel() {
if (!this.isFilterActive()) { if (!this.isFilterActive()) {
return null; return null;
} }
return { value: valueRef.current };
return { value };
}, },
setModel(model?: { value: string[] } | null) { setModel(model?: { value: string[] } | null) {
setValue(!model ? [] : model.value); valueRef.current = !model ? [] : model.value;
setValue(valueRef.current);
}, },
}; };
}); });
const onChange = (event: ChangeEvent<HTMLInputElement>) => { const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue( valueRef.current = event.target.checked
event.target.checked
? [...value, event.target.value] ? [...value, event.target.value]
: value.filter((v) => v !== event.target.value) : value.filter((v) => v !== event.target.value);
); setValue(valueRef.current);
}; };
useEffect(() => { useEffect(() => {
@ -77,7 +83,7 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
<button <button
type="button" type="button"
className="ag-standard-button ag-filter-apply-panel-button" className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => setValue([])} onClick={() => setValue((valueRef.current = []))}
> >
{t('Reset')} {t('Reset')}
</button> </button>

View File

@ -15,8 +15,8 @@ export const useInitialMargin = (
marketId: OrderSubmissionBody['orderSubmission']['marketId'], marketId: OrderSubmissionBody['orderSubmission']['marketId'],
order?: OrderSubmissionBody['orderSubmission'] order?: OrderSubmissionBody['orderSubmission']
) => { ) => {
const { pubKey: partyId } = useVegaWallet(); const { pubKey } = useVegaWallet();
const commonVariables = { marketId, partyId: partyId || '' }; const commonVariables = { marketId, partyId: pubKey || '' };
const { data: marketData } = useDataProvider({ const { data: marketData } = useDataProvider({
dataProvider: marketDataProvider, dataProvider: marketDataProvider,
variables: { marketId }, variables: { marketId },
@ -24,7 +24,7 @@ export const useInitialMargin = (
const { data: activeVolumeAndMargin } = useDataProvider({ const { data: activeVolumeAndMargin } = useDataProvider({
dataProvider: volumeAndMarginProvider, dataProvider: volumeAndMarginProvider,
variables: commonVariables, variables: commonVariables,
skip: !partyId, skip: !pubKey,
}); });
const { data: marketInfo } = useDataProvider({ const { data: marketInfo } = useDataProvider({
dataProvider: marketInfoProvider, dataProvider: marketInfoProvider,

View File

@ -38,10 +38,10 @@ export const TransferTooltipCellComponent = ({
); );
}; };
const defaultRangeFilter = { start: formatRFC3339(subDays(Date.now(), 7)) }; const defaultValue = { start: formatRFC3339(subDays(Date.now(), 7)) };
const dateRangeFilterParams = { const dateRangeFilterParams = {
maxNextDays: 0, maxNextDays: 0,
defaultRangeFilter, defaultValue,
}; };
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>; type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;

View File

@ -150,7 +150,7 @@ export const update = (
}); });
}; };
export const ordersProvider = makeDataProvider< const ordersProvider = makeDataProvider<
OrdersQuery, OrdersQuery,
ReturnType<typeof getData>, ReturnType<typeof getData>,
OrdersUpdateSubscription, OrdersUpdateSubscription,
@ -165,11 +165,36 @@ export const ordersProvider = makeDataProvider<
pagination: { pagination: {
getPageInfo, getPageInfo,
append, append,
first: 100, first: 1000,
}, },
additionalContext: { isEnlargedTimeout: true }, additionalContext: { isEnlargedTimeout: true },
}); });
export const activeOrdersProvider = makeDerivedDataProvider<
ReturnType<typeof getData>,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, variables) =>
ordersProvider(callback, client, {
partyId: variables.partyId,
filter: {
status: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
},
}),
],
(partsData, variables, prevData, parts, subscriptions) => {
if (!parts[0].isUpdate && subscriptions && subscriptions[0].load) {
subscriptions[0].load();
}
const orders = partsData[0] as ReturnType<typeof getData>;
return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id)
: orders;
}
);
export const ordersWithMarketProvider = makeDerivedDataProvider< export const ordersWithMarketProvider = makeDerivedDataProvider<
(OrderEdge | null)[], (OrderEdge | null)[],
Order[], Order[],
@ -193,63 +218,20 @@ export const ordersWithMarketProvider = makeDerivedDataProvider<
combineInsertionData<Order> combineInsertionData<Order>
); );
const hasActiveOrderProviderInternal = makeDataProvider<
OrdersQuery,
boolean,
OrdersUpdateSubscription,
ReturnType<typeof getDelta>,
OrdersQueryVariables
>({
query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument,
update: (
data: boolean | null,
delta: ReturnType<typeof getDelta>,
reload: () => void
) => {
const orders = delta?.filter(
(order) => !(order.peggedOrder || order.liquidityProvisionId)
);
if (!orders?.length) {
return data;
}
const hasActiveOrders = orders.some(
(order) => order.status === OrderStatus.STATUS_ACTIVE
);
if (hasActiveOrders) {
return true;
} else if (data && !hasActiveOrders) {
reload();
}
return data;
},
getData: (responseData: OrdersQuery | null) => {
const hasActiveOrder = !!responseData?.party?.ordersConnection?.edges?.some(
(order) => !(order.node.peggedOrder || order.node.liquidityProvision)
);
return hasActiveOrder;
},
getDelta,
});
export const hasActiveOrderProvider = makeDerivedDataProvider< export const hasActiveOrderProvider = makeDerivedDataProvider<
boolean, boolean,
never, never,
{ partyId: string; marketId?: string } { partyId: string; marketId?: string }
>( >([activeOrdersProvider], (parts) => !!parts[0].length);
[
(callback, client, { partyId, marketId }) => export const hasAmendableOrderProvider = makeDerivedDataProvider<
hasActiveOrderProviderInternal(callback, client, { boolean,
marketIds: marketId ? [marketId] : undefined, never,
filter: { { partyId: string; marketId?: string }
status: [OrderStatus.STATUS_ACTIVE], >([activeOrdersProvider], (parts) => {
excludeLiquidity: true, const activeOrders = parts[0] as ReturnType<typeof getData>;
}, const hasAmendableOrder = activeOrders.some(
pagination: { (edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder)
first: 1, );
}, return hasAmendableOrder;
partyId, });
} as OrdersQueryVariables),
],
(parts) => parts[0]
);

View File

@ -8,7 +8,7 @@ import type { GridReadyEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list/order-list'; import { OrderListTable } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data'; import { useOrderListData } from './use-order-list-data';
import { useHasActiveOrder } from '../../order-hooks/use-has-active-order'; import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
import type { Filter, Sort } from './use-order-list-data'; import type { Filter, Sort } from './use-order-list-data';
import { useBottomPlaceholder } from '@vegaprotocol/react-helpers'; import { useBottomPlaceholder } from '@vegaprotocol/react-helpers';
import { OrderStatus } from '@vegaprotocol/types'; import { OrderStatus } from '@vegaprotocol/types';
@ -16,6 +16,7 @@ import {
normalizeOrderAmendment, normalizeOrderAmendment,
useVegaTransactionStore, useVegaTransactionStore,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
import isEqual from 'lodash/isEqual';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet'; import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog'; import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order, OrderEdge } from '../order-data-provider'; import type { Order, OrderEdge } from '../order-data-provider';
@ -28,27 +29,18 @@ export interface OrderListManagerProps {
enforceBottomPlaceholder?: boolean; enforceBottomPlaceholder?: boolean;
} }
const CancelAllOrdersButton = ({ const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
onClick,
marketId,
}: {
onClick: (marketId?: string) => void;
marketId?: string;
}) => {
const hasActiveOrder = useHasActiveOrder(marketId);
return hasActiveOrder ? (
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded"> <div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button <Button
variant="primary" variant="primary"
size="sm" size="sm"
onClick={() => onClick(marketId)} onClick={onClick}
data-testid="cancelAll" data-testid="cancelAll"
> >
{t('Cancel all')} {t('Cancel all')}
</Button> </Button>
</div> </div>
) : null; );
};
const initialFilter: Filter = { const initialFilter: Filter = {
status: { status: {
@ -68,9 +60,10 @@ export const OrderListManager = ({
const scrolledToTop = useRef(false); const scrolledToTop = useRef(false);
const [sort, setSort] = useState<Sort[] | undefined>(); const [sort, setSort] = useState<Sort[] | undefined>();
const [filter, setFilter] = useState<Filter | undefined>(initialFilter); const [filter, setFilter] = useState<Filter | undefined>(initialFilter);
const filterRef = useRef(initialFilter);
const [editOrder, setEditOrder] = useState<Order | null>(null); const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create); const create = useVegaTransactionStore((state) => state.create);
const hasActiveOrder = useHasActiveOrder(marketId); const hasAmendableOrder = useHasAmendableOrder(marketId);
const { data, error, loading, reload } = useOrderListData({ const { data, error, loading, reload } = useOrderListData({
partyId, partyId,
@ -86,12 +79,16 @@ export const OrderListManager = ({
...bottomPlaceholderProps ...bottomPlaceholderProps
} = useBottomPlaceholder<Order>({ } = useBottomPlaceholder<Order>({
gridRef, gridRef,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasActiveOrder, disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
}); });
const onFilterChanged = useCallback( const onFilterChanged = useCallback(
(event: FilterChangedEvent) => { (event: FilterChangedEvent) => {
const updatedFilter = event.api.getFilterModel(); const updatedFilter = event.api.getFilterModel();
if (isEqual(updatedFilter, filterRef.current)) {
return;
}
filterRef.current = updatedFilter;
if (Object.keys(updatedFilter).length) { if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter); setFilter(updatedFilter);
} else { } else {
@ -142,16 +139,13 @@ export const OrderListManager = ({
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0); setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]); }, [data]);
const cancelAll = useCallback( const cancelAll = useCallback(() => {
(marketId?: string) => {
create({ create({
orderCancellation: { orderCancellation: {
marketId, marketId,
}, },
}); });
}, }, [create, marketId]);
[create]
);
const extractedData = const extractedData =
data && !loading data && !loading
? data ? data
@ -188,8 +182,8 @@ export const OrderListManager = ({
/> />
</div> </div>
</div> </div>
{!isReadOnly && ( {!isReadOnly && hasAmendableOrder && (
<CancelAllOrdersButton onClick={cancelAll} marketId={marketId} /> <CancelAllOrdersButton onClick={cancelAll} />
)} )}
{editOrder && ( {editOrder && (
<OrderEditDialog <OrderEditDialog

View File

@ -71,17 +71,27 @@ export const useOrderListData = ({
// define variable as const to get type safety, using generic with useMemo resulted in lost type safety // define variable as const to get type safety, using generic with useMemo resulted in lost type safety
const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = { const allVars: OrdersQueryVariables & OrdersUpdateSubscriptionVariables = {
partyId, partyId,
filter: {
dateRange: filter?.updatedAt?.value,
status: filter?.status?.value,
timeInForce: filter?.timeInForce?.value,
types: filter?.type?.value,
},
pagination: {
first: 1000,
},
}; };
if (
filter?.updatedAt?.value ||
filter?.status?.value.length ||
filter?.timeInForce?.value.length ||
filter?.type?.value.length
) {
allVars.filter = {};
if (filter?.updatedAt?.value) {
allVars.filter.dateRange = filter?.updatedAt?.value;
}
if (filter?.status?.value.length) {
allVars.filter.status = filter?.status?.value;
}
if (filter?.timeInForce?.value.length) {
allVars.filter.timeInForce = filter?.timeInForce?.value;
}
if (filter?.type?.value.length) {
allVars.filter.types = filter?.type?.value;
}
}
return allVars; return allVars;
}, [partyId, filter]); }, [partyId, filter]);

View File

@ -1,5 +1,4 @@
export * from './__generated__/OrdersSubscription'; export * from './__generated__/OrdersSubscription';
export * from './use-has-active-order'; export * from './use-has-amendable-order';
export * from './use-order-update'; export * from './use-order-update';
export * from './use-pending-orders-volume';
export * from './use-order-store'; export * from './use-order-store';

View File

@ -1,17 +1,17 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { hasActiveOrderProvider } from '../components/order-data-provider/'; import { hasAmendableOrderProvider } from '../components/order-data-provider';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
export const useHasActiveOrder = (marketId?: string) => { export const useHasAmendableOrder = (marketId?: string) => {
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const [hasActiveOrder, setHasActiveOrder] = useState(false); const [hasAmendableOrder, setHasAmendableOrder] = useState(false);
const update = useCallback(({ data }: { data: boolean | null }) => { const update = useCallback(({ data }: { data: boolean | null }) => {
setHasActiveOrder(Boolean(data)); setHasAmendableOrder(Boolean(data));
return true; return true;
}, []); }, []);
useDataProvider({ useDataProvider({
dataProvider: hasActiveOrderProvider, dataProvider: hasAmendableOrderProvider,
update, update,
variables: { variables: {
partyId: pubKey || '', partyId: pubKey || '',
@ -20,5 +20,5 @@ export const useHasActiveOrder = (marketId?: string) => {
skip: !pubKey, skip: !pubKey,
}); });
return hasActiveOrder; return hasAmendableOrder;
}; };

View File

@ -1,74 +0,0 @@
import { useState, useCallback } from 'react';
import { OrderStatus, Side } from '@vegaprotocol/types';
import { ordersProvider } from '../components/order-data-provider/order-data-provider';
import type { OrderFieldsFragment } from '../components/order-data-provider/__generated__/Orders';
import type { Edge } from '@vegaprotocol/utils';
import { useDataProvider } from '@vegaprotocol/react-helpers';
const sumVolume = (orders: (Edge<OrderFieldsFragment> | null)[], side: Side) =>
orders
.reduce(
(sum, order) =>
order?.node.side === side
? sum +
BigInt(
order?.node.status === OrderStatus.STATUS_PARTIALLY_FILLED
? order?.node.remaining
: order?.node.size
)
: sum,
BigInt(0)
)
.toString();
export const useActiveOrdersVolumeAndMargin = (
partyId: string | null | undefined,
marketId: string
) => {
const [buyVolume, setBuyVolume] = useState<string | undefined>();
const [sellVolume, setSellVolume] = useState<string | undefined>();
const [buyInitialMargin, setBuyInitialMargin] = useState<
string | undefined
>();
const [sellInitialMargin, setSellInitialMargin] = useState<
string | undefined
>();
const update = useCallback(
({ data }: { data: (Edge<OrderFieldsFragment> | null)[] | null }) => {
if (!data) {
setBuyVolume(undefined);
setSellVolume(undefined);
setBuyInitialMargin(undefined);
setSellInitialMargin(undefined);
} else {
setBuyVolume(sumVolume(data, Side.SIDE_BUY));
setSellVolume(sumVolume(data, Side.SIDE_SELL));
}
return true;
},
[]
);
useDataProvider({
dataProvider: ordersProvider,
update,
variables: {
partyId: partyId || '',
marketIds: [marketId],
filter: {
status: [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARTIALLY_FILLED,
],
},
},
skip: !partyId,
});
return buyVolume || sellVolume
? {
buyVolume,
sellVolume,
buyInitialMargin,
sellInitialMargin,
}
: undefined;
};

View File

@ -30,12 +30,12 @@ import {
import { marginsDataProvider } from './margin-data-provider'; import { marginsDataProvider } from './margin-data-provider';
import { calculateMargins } from './margin-calculator'; import { calculateMargins } from './margin-calculator';
import type { Edge } from '@vegaprotocol/utils'; import type { Edge } from '@vegaprotocol/utils';
import { OrderStatus, Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
import { marketInfoProvider } from '@vegaprotocol/market-info'; import { marketInfoProvider } from '@vegaprotocol/market-info';
import type { MarketInfoQuery } from '@vegaprotocol/market-info'; import type { MarketInfoQuery } from '@vegaprotocol/market-info';
import { marketDataProvider } from '@vegaprotocol/market-list'; import { marketDataProvider } from '@vegaprotocol/market-list';
import type { MarketData } from '@vegaprotocol/market-list'; import type { MarketData } from '@vegaprotocol/market-list';
import { ordersProvider } from '@vegaprotocol/orders'; import { activeOrdersProvider } from '@vegaprotocol/orders';
import type { OrderFieldsFragment } from '@vegaprotocol/orders'; import type { OrderFieldsFragment } from '@vegaprotocol/orders';
import type { PositionStatus } from '@vegaprotocol/types'; import type { PositionStatus } from '@vegaprotocol/types';
@ -350,12 +350,9 @@ export const volumeAndMarginProvider = makeDerivedDataProvider<
>( >(
[ [
(callback, client, { partyId, marketId }) => (callback, client, { partyId, marketId }) =>
ordersProvider(callback, client, { activeOrdersProvider(callback, client, {
partyId, partyId,
marketIds: [marketId], marketId,
filter: {
status: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
},
}), }),
(callback, client, variables) => (callback, client, variables) =>
marketDataProvider(callback, client, { marketId: variables.marketId }), marketDataProvider(callback, client, { marketId: variables.marketId }),

View File

@ -1,10 +1,10 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import isEqual from 'lodash/isEqual'; import isEqualWith from 'lodash/isEqualWith';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { usePrevious } from './use-previous';
import type { OperationVariables } from '@apollo/client'; import type { OperationVariables } from '@apollo/client';
import type { Subscribe, Load, UpdateCallback } from '@vegaprotocol/utils'; import type { Subscribe, Load, UpdateCallback } from '@vegaprotocol/utils';
import { variablesIsEqualCustomizer } from '@vegaprotocol/utils';
export interface useDataProviderParams< export interface useDataProviderParams<
Data, Data,
@ -62,13 +62,19 @@ export const useDataProvider = <
const flushRef = useRef<(() => void) | undefined>(undefined); const flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined); const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined); const loadRef = useRef<Load<Data> | undefined>(undefined);
const prevVariables = usePrevious(props.variables); const variablesRef = useRef<Variables>(props.variables);
const [variables, setVariables] = useState(props.variables); const variables = useMemo(() => {
useEffect(() => { if (
if (!isEqual(prevVariables, props.variables)) { !isEqualWith(
setVariables(props.variables); variablesRef.current,
props.variables,
variablesIsEqualCustomizer
)
) {
variablesRef.current = props.variables;
} }
}, [props.variables, prevVariables]); return variablesRef.current;
}, [props.variables]);
const flush = useCallback(() => { const flush = useCallback(() => {
if (flushRef.current) { if (flushRef.current) {
flushRef.current(); flushRef.current();

View File

@ -45,7 +45,7 @@ type CombinedData = {
type SubscriptionData = QueryData; type SubscriptionData = QueryData;
type Delta = Data; type Delta = Data;
type Variables = { var: string }; type Variables = { var: string; filter?: string[] };
const update = jest.fn< const update = jest.fn<
ReturnType<Update<Data, Delta, Variables>>, ReturnType<Update<Data, Delta, Variables>>,
@ -231,10 +231,14 @@ describe('data provider', () => {
clientSubscribeSubscribe.mockClear(); clientSubscribeSubscribe.mockClear();
}); });
it('memoize instance and unsubscribe if no subscribers', () => { it('memoize instance and unsubscribe if no subscribers', () => {
const subscription1 = subscribe(jest.fn(), client, variables); const subscription1 = subscribe(jest.fn(), client, {
const subscription2 = subscribe(jest.fn(), client, { ...variables }); ...variables,
// const subscription1 = subscribe(jest.fn(), client); filter: ['1', '2'],
// const subscription2 = subscribe(jest.fn(), client); });
const subscription2 = subscribe(jest.fn(), client, {
...variables,
filter: ['2', '1'],
});
expect(clientSubscribeSubscribe.mock.calls.length).toEqual(1); expect(clientSubscribeSubscribe.mock.calls.length).toEqual(1);
subscription1.unsubscribe(); subscription1.unsubscribe();
expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(0); expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(0);

View File

@ -10,7 +10,7 @@ import type {
} from '@apollo/client'; } from '@apollo/client';
import type { GraphQLErrors } from '@apollo/client/errors'; import type { GraphQLErrors } from '@apollo/client/errors';
import type { Subscription } from 'zen-observable-ts'; import type { Subscription } from 'zen-observable-ts';
import isEqual from 'lodash/isEqual'; import isEqualWith from 'lodash/isEqualWith';
import { isNotFoundGraphQLError } from './apollo-client'; import { isNotFoundGraphQLError } from './apollo-client';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
interface UpdateData<Data, Delta> { interface UpdateData<Data, Delta> {
@ -512,11 +512,26 @@ function makeDataProviderInternal<
}; };
} }
/**
* Compares two arrays assuming that they are sets of primitive values, used to compare gql query variables
*/
export const variablesIsEqualCustomizer: NonNullable<
Parameters<typeof isEqualWith>['2']
> = (value, other) => {
if (Array.isArray(value) && Array.isArray(other)) {
return (
value.length === other.length &&
new Set([...value, ...other]).size === value.length
);
}
return undefined;
};
/** /**
* Memoizes data provider instances using query variables as cache key * Memoizes data provider instances using query variables as cache key
* *
* @param fn * @param fn
* @returns subscibe function * @returns subscribe function
*/ */
const memoize = < const memoize = <
Data, Data,
@ -530,7 +545,9 @@ const memoize = <
variables?: Variables; variables?: Variables;
}[] = []; }[] = [];
return (variables?: Variables) => { return (variables?: Variables) => {
const cached = cache.find((c) => isEqual(c.variables, variables)); const cached = cache.find((c) =>
isEqualWith(c.variables, variables, variablesIsEqualCustomizer)
);
if (cached) { if (cached) {
return cached.subscribe; return cached.subscribe;
} }
@ -582,8 +599,10 @@ export function makeDataProvider<
const getInstance = memoize<Data, Delta, Variables>(() => const getInstance = memoize<Data, Delta, Variables>(() =>
makeDataProviderInternal(params) makeDataProviderInternal(params)
); );
return (callback, client, variables) => return (callback, client, variables) => {
getInstance(variables)(callback, client, variables); const instance = getInstance(variables)(callback, client, variables);
return instance;
};
} }
/** /**
@ -605,7 +624,9 @@ export type CombineDerivedData<
> = ( > = (
data: DerivedPart<Variables>['data'][], data: DerivedPart<Variables>['data'][],
variables: Variables, variables: Variables,
prevData: Data | null prevData: Data | null,
parts: DerivedPart<Variables>[],
subscriptions?: ReturnType<DependencySubscribe<Variables>>[]
) => Data | null; ) => Data | null;
export type CombineDerivedDelta< export type CombineDerivedDelta<
@ -687,7 +708,9 @@ function makeDerivedDataProviderInternal<
? combineData( ? combineData(
parts.map((part) => part.data), parts.map((part) => part.data),
variables, variables,
data data,
parts,
subscriptions
) )
: data; : data;
if ( if (