chore(trading): cleanup paginated data solution, improve performance (#4036)

This commit is contained in:
Bartłomiej Głownia 2023-06-10 04:02:23 +02:00 committed by GitHub
parent bf6c13f523
commit 2e7a6a6458
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 541 additions and 1029 deletions

View File

@ -38,7 +38,6 @@ type Data = Item[];
type QueryData = { type QueryData = {
data: Data; data: Data;
pageInfo?: PageInfo; pageInfo?: PageInfo;
totalCount?: number;
}; };
type CombinedData = { type CombinedData = {
@ -115,7 +114,6 @@ const paginatedSubscribe = makeDataProvider<
first, first,
append: defaultAppend, append: defaultAppend,
getPageInfo: (r) => r?.pageInfo ?? null, getPageInfo: (r) => r?.pageInfo ?? null,
getTotalCount: (r) => r?.totalCount,
}, },
}); });
@ -373,32 +371,10 @@ describe('data provider', () => {
subscription.unsubscribe(); subscription.unsubscribe();
}); });
it('fills data with nulls if pagination is enabled', async () => { it('loads requested data blocks', async () => {
const totalCount = 1000;
const data: Item[] = new Array(first).fill(null).map((v, i) => ({
cursor: i.toString(),
node: {
id: i.toString(),
},
}));
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data,
totalCount,
pageInfo: {
hasNextPage: true,
},
});
expect(callback.mock.calls[1][0].data?.length).toBe(totalCount);
subscription.unsubscribe();
});
it('loads requested data blocks and inserts data with total count', async () => {
const totalCount = 1000;
const subscription = paginatedSubscribe(callback, client, variables); const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({ await resolveQuery({
data: generateData(), data: generateData(),
totalCount,
pageInfo: { pageInfo: {
hasNextPage: true, hasNextPage: true,
endCursor: '100', endCursor: '100',
@ -407,168 +383,25 @@ describe('data provider', () => {
// load next page // load next page
subscription.load && subscription.load(); subscription.load && subscription.load();
let lastQueryArgs = const lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({ expect(lastQueryArgs?.variables?.['pagination']).toEqual({
after: '100', after: '100',
first, first,
}); });
await resolveQuery({ await resolveQuery({
data: generateData(100), data: generateData(100),
pageInfo: { pageInfo: {
hasNextPage: true, hasNextPage: false,
endCursor: '200', endCursor: '200',
}, },
}); });
// load page with skip
subscription.load && subscription.load(500, 600);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '200',
first,
skip: 300,
});
await resolveQuery({
data: generateData(500),
pageInfo: {
hasNextPage: true,
endCursor: '600',
},
});
// load in the gap
subscription.load && subscription.load(400, 500);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '200',
first,
skip: 200,
});
await resolveQuery({
data: generateData(400),
pageInfo: {
hasNextPage: true,
endCursor: '500',
},
});
// load page after last block
subscription.load && subscription.load(700, 800);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '600',
first,
skip: 100,
});
await resolveQuery({
data: generateData(700),
pageInfo: {
hasNextPage: true,
endCursor: '800',
},
});
// load last page shorter than expected
subscription.load && subscription.load(950, 1050);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '800',
first,
skip: 150,
});
await resolveQuery({
data: generateData(950, 20),
pageInfo: {
hasNextPage: false,
endCursor: '970',
},
});
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(970);
// load next page when pageInfo.hasNextPage === false // load next page when pageInfo.hasNextPage === false
const clientQueryCallsLength = clientQuery.mock.calls.length; const clientQueryCallsLength = clientQuery.mock.calls.length;
subscription.load && subscription.load(); subscription.load && subscription.load();
expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength); expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength);
// load last page longer than expected
subscription.load && subscription.load(960, 1000);
lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({
after: '960',
first,
});
await resolveQuery({
data: generateData(960, 40),
pageInfo: {
hasNextPage: true,
endCursor: '1000',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(1000);
subscription.unsubscribe();
});
it('loads requested data blocks and inserts data without totalCount', async () => {
const totalCount = undefined;
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
totalCount,
pageInfo: {
hasNextPage: true,
endCursor: '100',
},
});
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
// load next page
subscription.load && subscription.load();
await resolveQuery({
data: generateData(100),
pageInfo: {
hasNextPage: true,
endCursor: '200',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
// load last page
subscription.load && subscription.load();
await resolveQuery({
data: generateData(200, 50),
pageInfo: {
hasNextPage: false,
endCursor: '250',
},
});
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(250);
subscription.unsubscribe();
});
it('sets total count when first page has no next page', async () => {
const subscription = paginatedSubscribe(callback, client, variables);
await resolveQuery({
data: generateData(),
pageInfo: {
hasNextPage: false,
endCursor: '100',
},
});
const lastCallbackArgs =
callback.mock.calls[callback.mock.calls.length - 1];
expect(lastCallbackArgs[0].totalCount).toBe(100);
subscription.unsubscribe(); subscription.unsubscribe();
}); });
@ -752,7 +585,7 @@ describe('derived data provider', () => {
subscription.load && subscription.load(); subscription.load && subscription.load();
const lastQueryArgs = const lastQueryArgs =
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
expect(lastQueryArgs?.variables?.pagination).toEqual({ expect(lastQueryArgs?.variables?.['pagination']).toEqual({
after: '100', after: '100',
first, first,
}); });

View File

@ -27,7 +27,6 @@ export interface UpdateCallback<Data, Delta> {
loading: boolean; loading: boolean;
loaded: boolean; loaded: boolean;
pageInfo: PageInfo | null; pageInfo: PageInfo | null;
totalCount?: number;
} }
): void; ): void;
} }
@ -40,9 +39,7 @@ export interface Reload {
(forceReset?: boolean): void; (forceReset?: boolean): void;
} }
type Pagination = Schema.Pagination & { type Pagination = Schema.Pagination;
skip?: number;
};
export interface PageInfo { export interface PageInfo {
startCursor?: string; startCursor?: string;
@ -83,12 +80,8 @@ export interface Append<Data> {
data: Data | null, data: Data | null,
insertionData: Data | null, insertionData: Data | null,
insertionPageInfo: PageInfo | null, insertionPageInfo: PageInfo | null,
pagination?: Pagination, pagination?: Pagination
totalCount?: number ): Data | null;
): {
data: Data | null;
totalCount?: number;
};
} }
interface GetData<QueryData, Data, Variables> { interface GetData<QueryData, Data, Variables> {
@ -99,10 +92,6 @@ interface GetPageInfo<QueryData> {
(queryData: QueryData): PageInfo | null; (queryData: QueryData): PageInfo | null;
} }
interface GetTotalCount<QueryData> {
(queryData: QueryData): number | undefined;
}
interface GetDelta<SubscriptionData, Delta, Variables> { interface GetDelta<SubscriptionData, Delta, Variables> {
( (
subscriptionData: SubscriptionData, subscriptionData: SubscriptionData,
@ -119,44 +108,32 @@ export interface Edge<T extends Node> extends Cursor {
node: T; node: T;
} }
export function defaultAppend<Data>( export function defaultAppend<T extends Cursor>(
data: Data | null, data: T[] | null,
insertionData: Data | null, insertionData: T[] | null,
insertionPageInfo: PageInfo | null, insertionPageInfo: PageInfo | null,
pagination?: Pagination, pagination?: Pagination
totalCount?: number
) { ) {
if (data && insertionData && insertionPageInfo) { if (data && insertionData && insertionPageInfo) {
if (!(data instanceof Array) || !(insertionData instanceof Array)) { if (!(data instanceof Array) || !(insertionData instanceof Array)) {
throw new Error( throw new Error(
'data needs to be instance of Edge[] when using pagination' 'data needs to be instance of Array[] when using pagination'
); );
} }
if (pagination?.after) { if (pagination?.after) {
if (data[data.length - 1].cursor === pagination?.after) {
return [...data, ...insertionData];
}
const cursors = data.map((item) => item && item.cursor); const cursors = data.map((item) => item && item.cursor);
const startIndex = cursors.lastIndexOf(pagination.after); const startIndex = cursors.lastIndexOf(pagination.after);
if (startIndex !== -1) { if (startIndex !== -1) {
const start = startIndex + 1 + (pagination.skip ?? 0); const start = startIndex + 1;
const end = start + insertionData.length; const updatedData = [...data.slice(0, start), ...insertionData];
let updatedData = [ return updatedData;
...data.slice(0, start),
...insertionData,
...data.slice(end),
];
if (!insertionPageInfo.hasNextPage && end !== (totalCount ?? 0)) {
// adjust totalCount if last page is shorter or longer than expected
totalCount = end;
updatedData = updatedData.slice(0, end);
}
return {
data: updatedData,
// increase totalCount if last page is longer than expected
totalCount: totalCount && Math.max(updatedData.length, totalCount),
};
} }
} }
} }
return { data, totalCount }; return data;
} }
interface DataProviderParams< interface DataProviderParams<
@ -175,7 +152,6 @@ interface DataProviderParams<
getDelta?: GetDelta<SubscriptionData, Delta, Variables>; getDelta?: GetDelta<SubscriptionData, Delta, Variables>;
pagination?: { pagination?: {
getPageInfo: GetPageInfo<QueryData>; getPageInfo: GetPageInfo<QueryData>;
getTotalCount?: GetTotalCount<QueryData>;
append: Append<Data>; append: Append<Data>;
first: number; first: number;
}; };
@ -245,7 +221,6 @@ function makeDataProviderInternal<
let client: ApolloClient<object>; let client: ApolloClient<object>;
let subscription: Subscription[] | undefined; let subscription: Subscription[] | undefined;
let pageInfo: PageInfo | null = null; let pageInfo: PageInfo | null = null;
let totalCount: number | undefined;
// notify single callback about current state, delta is passes optionally only if notify was invoked onNext // notify single callback about current state, delta is passes optionally only if notify was invoked onNext
const notify = ( const notify = (
@ -258,7 +233,6 @@ function makeDataProviderInternal<
loading, loading,
loaded, loaded,
pageInfo, pageInfo,
totalCount,
...updateData, ...updateData,
}); });
}; };
@ -301,59 +275,41 @@ function makeDataProviderInternal<
} }
}); });
const load = async (start?: number) => { const load = async () => {
if (!pagination) { if (!pagination) {
return Promise.reject(); return Promise.reject();
} }
if (!pageInfo?.hasNextPage) {
return null;
}
const paginationVariables: Pagination = { const paginationVariables: Pagination = {
first: pagination.first, first: pagination.first,
after: pageInfo?.endCursor,
}; };
if (start !== undefined && data instanceof Array) { if (data) {
if (!start) { const endCursor = (data as Cursor[])[(data as Cursor[]).length - 1]
paginationVariables.after = undefined; .cursor;
} else if (data && data[start - 1]) { if (endCursor) {
paginationVariables.after = (data[start - 1] as Cursor).cursor; paginationVariables.after = endCursor;
} else {
let skip = 1;
while (!data[start - 1 - skip] && skip <= start) {
skip += 1;
}
paginationVariables.skip = skip;
if (skip === start) {
paginationVariables.after = undefined;
} else {
paginationVariables.after = (data[start - 1 - skip] as Cursor).cursor;
}
} }
} else if (!pageInfo?.hasNextPage) {
return null;
} }
const res = await call(paginationVariables); const res = await call(paginationVariables);
const insertionData = getData(res.data, variables); const insertionData = getData(res.data, variables);
const insertionPageInfo = pagination.getPageInfo(res.data); const insertionPageInfo = pagination.getPageInfo(res.data);
({ data, totalCount } = pagination.append( data = pagination.append(
data, data,
insertionData, insertionData,
insertionPageInfo, insertionPageInfo,
paginationVariables, paginationVariables
totalCount );
));
pageInfo = insertionPageInfo; pageInfo = insertionPageInfo;
totalCount =
(pagination.getTotalCount && pagination.getTotalCount(res.data)) ??
totalCount;
notifyAll({ insertionData, isInsert: true }); notifyAll({ insertionData, isInsert: true });
return insertionData; return insertionData;
}; };
const setData = (updatedData: Data | null) => { const setData = (updatedData: Data | null) => {
data = updatedData; data = updatedData;
if (totalCount !== undefined && data instanceof Array) {
totalCount = data.length;
}
}; };
const subscriptionSubscribe = () => { const subscriptionSubscribe = () => {
@ -400,16 +356,6 @@ function makeDataProviderInternal<
); );
} }
pageInfo = pagination.getPageInfo(res.data); pageInfo = pagination.getPageInfo(res.data);
if (pageInfo && !pageInfo.hasNextPage) {
totalCount = data.length;
} else {
totalCount =
pagination.getTotalCount && pagination.getTotalCount(res.data);
}
if (data && totalCount && data.length < totalCount) {
data.push(...new Array(totalCount - data.length).fill(null));
}
} }
// if there was some updates received from subscription during initial query loading apply them on just received data // if there was some updates received from subscription during initial query loading apply them on just received data
if (update && data && updateQueue && updateQueue.length > 0) { if (update && data && updateQueue && updateQueue.length > 0) {
@ -417,9 +363,6 @@ function makeDataProviderInternal<
const delta = updateQueue.shift(); const delta = updateQueue.shift();
if (delta) { if (delta) {
setData(update(data, delta, reload, variables)); setData(update(data, delta, reload, variables));
if (totalCount !== undefined && data instanceof Array) {
totalCount = data.length;
}
} }
} }
} }
@ -590,7 +533,7 @@ const memoize = <
* @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can reload data provider * @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can reload data provider
* @param getData transforms received query data to format that will be stored in data provider * @param getData transforms received query data to format that will be stored in data provider
* @param getDelta transforms delta data to format that will be stored in data provider * @param getDelta transforms delta data to format that will be stored in data provider
* @param pagination pagination related functions { getPageInfo, getTotalCount, append, first } * @param pagination pagination related functions { getPageInfo, append, first }
* @returns Subscribe<Data, Delta> subscribe function * @returns Subscribe<Data, Delta> subscribe function
* @example * @example
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({ * const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({

View File

@ -12,23 +12,13 @@ export interface useDataProviderParams<
Variables extends OperationVariables | undefined = undefined Variables extends OperationVariables | undefined = undefined
> { > {
dataProvider: Subscribe<Data, Delta, Variables>; dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({ update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
delta,
data,
totalCount,
}: {
delta?: Delta;
data: Data | null;
totalCount?: number;
}) => boolean;
insert?: ({ insert?: ({
insertionData, insertionData,
data, data,
totalCount,
}: { }: {
insertionData?: Data | null; insertionData?: Data | null;
data: Data | null; data: Data | null;
totalCount?: number;
}) => boolean; }) => boolean;
variables: Variables; variables: Variables;
skipUpdates?: boolean; skipUpdates?: boolean;
@ -56,7 +46,6 @@ export const useDataProvider = <
}: useDataProviderParams<Data, Delta, Variables>) => { }: useDataProviderParams<Data, Delta, Variables>) => {
const client = useApolloClient(); const client = useApolloClient();
const [data, setData] = useState<Data | null>(null); const [data, setData] = useState<Data | null>(null);
const [totalCount, setTotalCount] = useState<number>();
const [loading, setLoading] = useState<boolean>(!skip); const [loading, setLoading] = useState<boolean>(!skip);
const [error, setError] = useState<Error | undefined>(undefined); const [error, setError] = useState<Error | undefined>(undefined);
const flushRef = useRef<(() => void) | undefined>(undefined); const flushRef = useRef<(() => void) | undefined>(undefined);
@ -101,7 +90,6 @@ export const useDataProvider = <
error, error,
loading, loading,
insertionData, insertionData,
totalCount,
isInsert, isInsert,
isUpdate, isUpdate,
loaded, loaded,
@ -116,19 +104,18 @@ export const useDataProvider = <
(skipUpdatesRef.current || (skipUpdatesRef.current ||
(!skipUpdatesRef.current && (!skipUpdatesRef.current &&
updateRef.current && updateRef.current &&
updateRef.current({ delta, data, totalCount }))) updateRef.current({ delta, data })))
) { ) {
return; return;
} }
if ( if (
isInsert && isInsert &&
insertRef.current && insertRef.current &&
insertRef.current({ insertionData, data, totalCount }) insertRef.current({ insertionData, data })
) { ) {
return; return;
} }
} }
setTotalCount(totalCount);
setData(data); setData(data);
if (!loading && !isUpdate && updateRef.current) { if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data }); updateRef.current({ data });
@ -150,7 +137,6 @@ export const useDataProvider = <
useEffect(() => { useEffect(() => {
setData(null); setData(null);
setError(undefined); setError(undefined);
setTotalCount(undefined);
if (updateRef.current) { if (updateRef.current) {
updateRef.current({ data: null }); updateRef.current({ data: null });
} }
@ -184,7 +170,6 @@ export const useDataProvider = <
flush, flush,
reload, reload,
load, load,
totalCount,
}; };
}; };

View File

@ -146,7 +146,7 @@ export const DealTicket = ({
}); });
const openVolume = useOpenVolume(pubKey, market.id) ?? '0'; const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
const orders = activeOrders const orders = activeOrders
? activeOrders.map<OrderInfo>(({ node: order }) => ({ ? activeOrders.map<OrderInfo>((order) => ({
isMarketOrder: order.type === OrderType.TYPE_MARKET, isMarketOrder: order.type === OrderType.TYPE_MARKET,
price: order.price, price: order.price,
remaining: order.remaining, remaining: order.remaining,

View File

@ -1,4 +1,3 @@
export * from './lib/fills-container'; export * from './lib/fills-container';
export * from './lib/use-fills-list';
export * from './lib/fills-data-provider'; export * from './lib/fills-data-provider';
export * from './lib/__generated__/Fills'; export * from './lib/__generated__/Fills';

View File

@ -48,28 +48,32 @@ query Fills($filter: TradesFilter, $pagination: Pagination) {
} }
} }
subscription FillsEvent($filter: TradesSubscriptionFilter!) { fragment FillUpdateFields on TradeUpdate {
tradesStream(filter: $filter) { id
id marketId
marketId buyOrder
buyOrder sellOrder
sellOrder buyerId
buyerId sellerId
sellerId aggressor
aggressor price
price size
size createdAt
createdAt type
type buyerFee {
buyerFee { makerFee
makerFee infrastructureFee
infrastructureFee liquidityFee
liquidityFee }
} sellerFee {
sellerFee { makerFee
makerFee infrastructureFee
infrastructureFee liquidityFee
liquidityFee }
} }
subscription FillsEvent($filter: TradesSubscriptionFilter!) {
tradesStream(filter: $filter) {
...FillUpdateFields
} }
} }

View File

@ -15,6 +15,8 @@ export type FillsQueryVariables = Types.Exact<{
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null }; export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
export type FillsEventSubscriptionVariables = Types.Exact<{ export type FillsEventSubscriptionVariables = Types.Exact<{
filter: Types.TradesSubscriptionFilter; filter: Types.TradesSubscriptionFilter;
}>; }>;
@ -60,6 +62,31 @@ export const FillEdgeFragmentDoc = gql`
cursor cursor
} }
${FillFieldsFragmentDoc}`; ${FillFieldsFragmentDoc}`;
export const FillUpdateFieldsFragmentDoc = gql`
fragment FillUpdateFields on TradeUpdate {
id
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
}
`;
export const FillsDocument = gql` export const FillsDocument = gql`
query Fills($filter: TradesFilter, $pagination: Pagination) { query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) { trades(filter: $filter, pagination: $pagination) {
@ -107,30 +134,10 @@ export type FillsQueryResult = Apollo.QueryResult<FillsQuery, FillsQueryVariable
export const FillsEventDocument = gql` export const FillsEventDocument = gql`
subscription FillsEvent($filter: TradesSubscriptionFilter!) { subscription FillsEvent($filter: TradesSubscriptionFilter!) {
tradesStream(filter: $filter) { tradesStream(filter: $filter) {
id ...FillUpdateFields
marketId
buyOrder
sellOrder
buyerId
sellerId
aggressor
price
size
createdAt
type
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
} }
} }
`; ${FillUpdateFieldsFragmentDoc}`;
/** /**
* __useFillsEventSubscription__ * __useFillsEventSubscription__

View File

@ -1,74 +1,34 @@
import produce from 'immer';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import {} from '@vegaprotocol/utils'; import type { PageInfo, Cursor } from '@vegaprotocol/data-provider';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import { import {
makeDataProvider, makeDataProvider,
makeDerivedDataProvider, makeDerivedDataProvider,
defaultAppend as append, defaultAppend as append,
paginatedCombineDelta as combineDelta,
paginatedCombineInsertionData as combineInsertionData,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets'; import { marketsMapProvider } from '@vegaprotocol/markets';
import { FillsDocument, FillsEventDocument } from './__generated__/Fills'; import { FillsDocument, FillsEventDocument } from './__generated__/Fills';
import type { import type {
FillsQuery, FillsQuery,
FillsQueryVariables, FillsQueryVariables,
FillFieldsFragment, FillFieldsFragment,
FillEdgeFragment,
FillsEventSubscription, FillsEventSubscription,
FillUpdateFieldsFragment,
FillsEventSubscriptionVariables,
} from './__generated__/Fills'; } from './__generated__/Fills';
const update = (
data: FillEdgeFragment[] | null,
delta: FillsEventSubscription['tradesStream']
) => {
return produce(data, (draft) => {
orderBy(delta, 'createdAt').forEach((node) => {
if (draft === null) {
return;
}
const index = draft.findIndex((edge) => edge?.node.id === node.id);
if (index !== -1) {
if (draft[index]?.node) {
Object.assign(draft[index]?.node as FillFieldsFragment, node);
}
} else {
const firstNode = draft[0]?.node;
if (
(firstNode && node.createdAt >= firstNode.createdAt) ||
!firstNode
) {
const { buyerId, sellerId, marketId, ...trade } = node;
draft.unshift({
node: {
...trade,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
},
cursor: '',
__typename: 'TradeEdge',
});
}
}
});
});
};
export type Trade = Omit<FillFieldsFragment, 'market'> & { export type Trade = Omit<FillFieldsFragment, 'market'> & {
market?: Market; market?: Market;
isLastPlaceholder?: boolean; isLastPlaceholder?: boolean;
}; };
export type TradeEdge = Edge<Trade>;
const getData = (responseData: FillsQuery | null): FillEdgeFragment[] => const getData = (
responseData?.trades?.edges || []; responseData: FillsQuery | null
): (FillFieldsFragment & Cursor)[] =>
responseData?.trades?.edges.map<FillFieldsFragment & Cursor>((edge) => ({
...edge.node,
cursor: edge.cursor,
})) || [];
const getPageInfo = (responseData: FillsQuery | null): PageInfo | null => const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
responseData?.trades?.pageInfo || null; responseData?.trades?.pageInfo || null;
@ -76,16 +36,65 @@ const getPageInfo = (responseData: FillsQuery | null): PageInfo | null =>
const getDelta = (subscriptionData: FillsEventSubscription) => const getDelta = (subscriptionData: FillsEventSubscription) =>
subscriptionData.tradesStream || []; subscriptionData.tradesStream || [];
const mapFillUpdateToFill = (
fillUpdate: FillUpdateFieldsFragment
): FillFieldsFragment => {
const { buyerId, sellerId, marketId, ...fill } = fillUpdate;
return {
...fill,
__typename: 'Trade',
market: {
__typename: 'Market',
id: marketId,
},
buyer: { id: buyerId, __typename: 'Party' },
seller: { id: buyerId, __typename: 'Party' },
};
};
const mapFillUpdateToFillWithMarket =
(markets: Record<string, Market>) =>
(fillUpdate: FillUpdateFieldsFragment): Trade => {
const { market, ...fill } = mapFillUpdateToFill(fillUpdate);
return {
...fill,
market: markets[market.id],
};
};
const update = <T extends Omit<FillFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
variables: FillsQueryVariables,
mapDeltaToData: (delta: FillUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
orderBy(delta, 'createdAt', 'desc').forEach((fillUpdate) => {
const index = data?.findIndex((fill) => fill.id === fillUpdate.id) ?? -1;
if (index !== -1) {
updatedData[index] = {
...updatedData[index],
...mapDeltaToData(fillUpdate),
};
} else if (!data?.length || fillUpdate.createdAt >= data[0].createdAt) {
updatedData.unshift(mapDeltaToData(fillUpdate));
}
});
return updatedData;
};
export const fillsProvider = makeDataProvider< export const fillsProvider = makeDataProvider<
Parameters<typeof getData>['0'], Parameters<typeof getData>['0'],
ReturnType<typeof getData>, ReturnType<typeof getData>,
Parameters<typeof getDelta>['0'], Parameters<typeof getDelta>['0'],
ReturnType<typeof getDelta>, ReturnType<typeof getDelta>,
FillsQueryVariables FillsQueryVariables,
FillsEventSubscriptionVariables
>({ >({
query: FillsDocument, query: FillsDocument,
subscriptionQuery: FillsEventDocument, subscriptionQuery: FillsEventDocument,
update, update: (data, delta, reload, variables) =>
update(data, delta, variables, mapFillUpdateToFill),
getData, getData,
getDelta, getDelta,
pagination: { pagination: {
@ -93,30 +102,41 @@ export const fillsProvider = makeDataProvider<
append, append,
first: 100, first: 100,
}, },
getSubscriptionVariables: ({ filter }) => {
const variables: FillsEventSubscriptionVariables = { filter: {} };
if (filter) {
variables.filter = {
partyIds: filter.partyIds,
marketIds: filter.marketIds,
};
}
return variables;
},
}); });
export const fillsWithMarketProvider = makeDerivedDataProvider< export const fillsWithMarketProvider = makeDerivedDataProvider<
(TradeEdge | null)[],
Trade[], Trade[],
never,
FillsQueryVariables FillsQueryVariables
>( >(
[ [
fillsProvider, fillsProvider,
(callback, client) => marketsProvider(callback, client, undefined), (callback, client) => marketsMapProvider(callback, client, undefined),
], ],
(partsData): (TradeEdge | null)[] => (partsData, variables, prevData, parts): Trade[] | null => {
(partsData[0] as ReturnType<typeof getData>)?.map( if (prevData && parts[0].isUpdate) {
(edge) => return update(
edge && { prevData,
cursor: edge.cursor, parts[0].delta as ReturnType<typeof getDelta>,
node: { variables,
...edge.node, mapFillUpdateToFillWithMarket(partsData[1] as Record<string, Market>)
market: (partsData[1] as Market[]).find( );
(market) => market.id === edge.node.market.id }
), return ((partsData[0] as ReturnType<typeof getData>) || []).map(
}, (trade) => ({
} ...trade,
) || null, market: (partsData[1] as Record<string, Market>)[trade.market.id],
combineDelta<Trade, ReturnType<typeof getDelta>['0']>, })
combineInsertionData<Trade> );
}
); );

View File

@ -1,10 +1,11 @@
import compact from 'lodash/compact';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react'; import { useRef } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table'; import { FillsTable } from './fills-table';
import { useFillsList } from './use-fills-list';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid'; import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import { fillsWithMarketProvider } from './fills-data-provider';
interface FillsManagerProps { interface FillsManagerProps {
partyId: string; partyId: string;
@ -20,31 +21,36 @@ export const FillsManager = ({
storeKey, storeKey,
}: FillsManagerProps) => { }: FillsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const scrolledToTop = useRef(true); const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
const { data, error } = useFillsList({ partyIds: [partyId],
partyId, };
marketId, if (marketId) {
gridRef, filter.marketIds = [marketId];
scrolledToTop, }
const { data, error } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update: ({ data }) => {
if (data?.length && gridRef.current?.api) {
gridRef.current?.api.setRowData(data);
return true;
}
return false;
},
variables: { filter },
}); });
const bottomPlaceholderProps = useBottomPlaceholder({ const bottomPlaceholderProps = useBottomPlaceholder({
gridRef, gridRef,
}); });
const fills = compact(data).map((e) => e.node);
return ( return (
<div className="h-full relative"> <FillsTable
<FillsTable ref={gridRef}
ref={gridRef} rowData={data}
rowData={fills} partyId={partyId}
partyId={partyId} onMarketClick={onMarketClick}
onMarketClick={onMarketClick} storeKey={storeKey}
storeKey={storeKey} {...bottomPlaceholderProps}
{...bottomPlaceholderProps} overlayNoRowsTemplate={error ? error.message : t('No fills')}
overlayNoRowsTemplate={error ? error.message : t('No fills')} />
/>
</div>
); );
}; };

View File

@ -1,95 +0,0 @@
import type { AgGridReact } from 'ag-grid-react';
import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { useFillsList } from './use-fills-list';
import type { TradeEdge } from './fills-data-provider';
let mockData = null;
let mockDataProviderData = {
data: mockData as (TradeEdge | null)[] | null,
error: undefined,
loading: true,
};
let updateMock: jest.Mock;
const mockDataProvider = jest.fn((args) => {
updateMock = args.update;
return mockDataProviderData;
});
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn((args) => mockDataProvider(args)),
}));
describe('useFillsList Hook', () => {
const mockRefreshAgGridApi = jest.fn();
const partyId = 'partyId';
const gridRef = {
current: {
api: {
refreshInfiniteCache: mockRefreshAgGridApi,
getModel: () => ({ getType: () => 'infinite' }),
},
} as unknown as AgGridReact,
};
const scrolledToTop = {
current: false,
};
afterEach(() => {
jest.clearAllMocks();
});
it('should return proper dataProvider results', () => {
const { result } = renderHook(
() => useFillsList({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: null,
error: undefined,
loading: true,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
});
it('return proper mocked results', () => {
mockData = [
{
node: {
id: 'data_id_1',
},
} as unknown as TradeEdge,
{
node: {
id: 'data_id_2',
},
} as unknown as TradeEdge,
];
mockDataProviderData = {
...mockDataProviderData,
data: mockData,
loading: false,
};
const { result } = renderHook(
() => useFillsList({ partyId, gridRef, scrolledToTop }),
{
wrapper: MockedProvider,
}
);
expect(result.current).toMatchObject({
data: mockData,
error: undefined,
loading: false,
addNewRows: expect.any(Function),
getRows: expect.any(Function),
});
updateMock({ data: mockData });
expect(mockRefreshAgGridApi).not.toHaveBeenCalled();
updateMock({ data: mockData });
expect(mockRefreshAgGridApi).toHaveBeenCalled();
});
});

View File

@ -1,123 +0,0 @@
import type { RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef } from 'react';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types';
import { updateGridData } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Trade, TradeEdge } from './fills-data-provider';
import { fillsWithMarketProvider } from './fills-data-provider';
interface Props {
partyId: string;
marketId?: string;
gridRef: RefObject<AgGridReact>;
scrolledToTop: RefObject<boolean>;
}
export const useFillsList = ({
partyId,
marketId,
gridRef,
scrolledToTop,
}: Props) => {
const dataRef = useRef<(TradeEdge | null)[] | null>(null);
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const placeholderAdded = useRef(-1);
const makeBottomPlaceholders = useCallback((trade?: Trade) => {
if (!trade) {
if (placeholderAdded.current >= 0) {
dataRef.current?.splice(placeholderAdded.current, 1);
}
placeholderAdded.current = -1;
} else if (placeholderAdded.current === -1) {
dataRef.current?.push({
node: { ...trade, id: `${trade?.id}-1`, isLastPlaceholder: true },
});
placeholderAdded.current = (dataRef.current?.length || 0) - 1;
}
}, []);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
return;
}
if (totalCountRef.current !== undefined) {
totalCountRef.current += newRows.current;
}
newRows.current = 0;
gridRef.current?.api?.refreshInfiniteCache();
}, [gridRef]);
const update = useCallback(
({
data,
delta,
}: {
data: (TradeEdge | null)[] | null;
delta?: Trade[];
}) => {
if (dataRef.current?.length) {
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += (delta || []).filter(
(trade) => trade.createdAt > createdAt
).length;
}
}
return updateGridData(dataRef, data, gridRef);
}
dataRef.current = data;
return false;
},
[gridRef, scrolledToTop]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: (TradeEdge | null)[] | null;
totalCount?: number;
}) => {
totalCountRef.current = totalCount;
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const filter: Types.TradesFilter & Types.TradesSubscriptionFilter = {
partyIds: [partyId],
};
if (marketId) {
filter.marketIds = [marketId];
}
const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update,
insert,
variables: { filter },
});
totalCountRef.current = totalCount;
const getRows = makeInfiniteScrollGetRows<TradeEdge>(
dataRef,
totalCountRef,
load,
newRows
);
return {
data,
error,
loading,
addNewRows,
getRows,
reload,
makeBottomPlaceholders,
};
};

View File

@ -1,22 +1,12 @@
import type { Asset } from '@vegaprotocol/assets'; import type { Asset } from '@vegaprotocol/assets';
import { assetsProvider } from '@vegaprotocol/assets'; import { assetsMapProvider } from '@vegaprotocol/assets';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets'; import { marketsMapProvider } from '@vegaprotocol/markets';
import { makeInfiniteScrollGetRows } from '@vegaprotocol/data-provider';
import { updateGridData } from '@vegaprotocol/datagrid';
import { import {
makeDataProvider, makeDataProvider,
makeDerivedDataProvider, makeDerivedDataProvider,
useDataProvider,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import type { AgGridReact } from 'ag-grid-react';
import produce from 'immer';
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';
import type { RefObject } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import type { Filter } from './ledger-manager';
import type { import type {
LedgerEntriesQuery, LedgerEntriesQuery,
LedgerEntriesQueryVariables, LedgerEntriesQueryVariables,
@ -30,171 +20,58 @@ export type LedgerEntry = LedgerEntryFragment & {
marketReceiver: Market | null | undefined; marketReceiver: Market | null | undefined;
}; };
export type AggregatedLedgerEntriesEdge = Schema.AggregatedLedgerEntriesEdge; type Edge = LedgerEntriesQuery['ledgerEntries']['edges'][number];
export type AggregatedLedgerEntriesNode = Omit<
AggregatedLedgerEntriesEdge, const isLedgerEntryEdge = (entry: Edge): entry is NonNullable<Edge> =>
'node' entry !== null;
> & {
node: LedgerEntry;
};
const getData = (responseData: LedgerEntriesQuery | null) => { const getData = (responseData: LedgerEntriesQuery | null) => {
return responseData?.ledgerEntries?.edges || []; return (
responseData?.ledgerEntries?.edges
.filter(isLedgerEntryEdge)
.map((edge) => edge.node) || []
);
}; };
export const update = ( const ledgerEntriesOnlyProvider = makeDataProvider<
data: ReturnType<typeof getData> | null, LedgerEntriesQuery,
delta: ReturnType<typeof getData>, ReturnType<typeof getData>,
reload: () => void, never,
variables: LedgerEntriesQueryVariables never,
) => { LedgerEntriesQueryVariables
if (!data) { >({
return data;
}
return produce(data, (draft) => {
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
const incoming = uniqBy(
orderBy(delta, (entry) => entry?.node.vegaTime, 'desc'),
'id'
);
// Add or update incoming orders
incoming.reverse().forEach((node) => {
const index = draft.findIndex(
(edge) => edge?.node.vegaTime === node?.node.vegaTime
);
const newer =
draft.length === 0 || node?.node.vegaTime >= draft[0]?.node.vegaTime;
let doesFilterPass = true;
if (
doesFilterPass &&
variables?.dateRange?.start &&
new Date(node?.node.vegaTime) <= new Date(variables?.dateRange?.start)
) {
doesFilterPass = false;
}
if (
doesFilterPass &&
variables?.dateRange?.end &&
new Date(node?.node.vegaTime) >= new Date(variables?.dateRange?.end)
) {
doesFilterPass = false;
}
if (index !== -1) {
if (doesFilterPass) {
// Object.assign(draft[index]?.node, node?.node);
if (newer) {
draft.unshift(...draft.splice(index, 1));
}
} else {
draft.splice(index, 1);
}
} else if (newer && doesFilterPass) {
draft.unshift(node);
}
});
});
};
const ledgerEntriesOnlyProvider = makeDataProvider({
query: LedgerEntriesDocument, query: LedgerEntriesDocument,
getData, getData,
getDelta: getData,
update,
additionalContext: { additionalContext: {
isEnlargedTimeout: true, isEnlargedTimeout: true,
}, },
}); });
export const ledgerEntriesProvider = makeDerivedDataProvider< export const ledgerEntriesProvider = makeDerivedDataProvider<
AggregatedLedgerEntriesNode[], LedgerEntry[],
AggregatedLedgerEntriesNode[], never,
LedgerEntriesQueryVariables LedgerEntriesQueryVariables
>( >(
[ [
ledgerEntriesOnlyProvider, ledgerEntriesOnlyProvider,
(callback, client) => assetsProvider(callback, client, undefined), (callback, client) => assetsMapProvider(callback, client, undefined),
(callback, client) => marketsProvider(callback, client, undefined), (callback, client) => marketsMapProvider(callback, client, undefined),
], ],
([entries, assets, markets]) => { (partsData) => {
return entries.map((edge: AggregatedLedgerEntriesEdge) => { const entries = partsData[0] as ReturnType<typeof getData>;
const entry = edge.node; const assets = partsData[1] as Record<string, Asset>;
const asset = assets.find((asset: Asset) => asset.id === entry.assetId); const markets = partsData[1] as Record<string, Market>;
const marketSender = markets.find( return entries.map((entry) => {
(market: Market) => market.id === entry.fromAccountMarketId const asset = entry.assetId
); ? (assets as Record<string, Asset>)[entry.assetId]
const marketReceiver = markets.find( : null;
(market: Market) => market.id === entry.toAccountMarketId const marketSender = entry.fromAccountMarketId
); ? markets[entry.fromAccountMarketId]
const cursor = edge?.cursor; : null;
return { const marketReceiver = entry.toAccountMarketId
node: { ...entry, asset, marketSender, marketReceiver }, ? markets[entry.toAccountMarketId]
cursor, : null;
}; return { ...entry, asset, marketSender, marketReceiver };
}); });
} }
); );
interface Props {
partyId: string;
filter?: Filter;
gridRef: RefObject<AgGridReact>;
}
export const useLedgerEntriesDataProvider = ({
partyId,
filter,
gridRef,
}: Props) => {
const dataRef = useRef<AggregatedLedgerEntriesEdge[] | null>(null);
const totalCountRef = useRef<number>();
const variables = useMemo<LedgerEntriesQueryVariables>(
() => ({
partyId,
dateRange: filter?.vegaTime?.value,
pagination: {
first: 5000,
},
}),
[partyId, filter?.vegaTime?.value]
);
const update = useCallback(
({ data }: { data: AggregatedLedgerEntriesEdge[] | null }) => {
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const insert = useCallback(
({
data,
totalCount,
}: {
data: AggregatedLedgerEntriesEdge[] | null;
totalCount?: number;
}) => {
totalCountRef.current = totalCount;
return updateGridData(dataRef, data, gridRef);
},
[gridRef]
);
const { data, error, loading, load, totalCount, reload } = useDataProvider({
dataProvider: ledgerEntriesProvider,
update,
insert,
variables,
skip: !variables.partyId,
});
totalCountRef.current = totalCount;
const getRows = makeInfiniteScrollGetRows<AggregatedLedgerEntriesEdge>(
dataRef,
totalCountRef,
load
);
return { loading, error, data, getRows, reload };
};

View File

@ -2,10 +2,12 @@ import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community'; import type { FilterChangedEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns'; import { subDays, formatRFC3339 } from 'date-fns';
import { useLedgerEntriesDataProvider } from './ledger-entries-data-provider'; import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
import { LedgerTable } from './ledger-table'; import { LedgerTable } from './ledger-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Types from '@vegaprotocol/types'; import type * as Types from '@vegaprotocol/types';
import { LedgerExportLink } from './ledger-export-link'; import { LedgerExportLink } from './ledger-export-link';
@ -26,10 +28,21 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter>(defaultFilter); const [filter, setFilter] = useState<Filter>(defaultFilter);
const { data, error } = useLedgerEntriesDataProvider({ const variables = useMemo<LedgerEntriesQueryVariables>(
partyId, () => ({
filter, partyId,
gridRef, dateRange: filter?.vegaTime?.value,
pagination: {
first: 5000,
},
}),
[partyId, filter?.vegaTime?.value]
);
const { data, error } = useDataProvider({
dataProvider: ledgerEntriesProvider,
variables,
skip: !variables.partyId,
}); });
const onFilterChanged = useCallback((event: FilterChangedEvent) => { const onFilterChanged = useCallback((event: FilterChangedEvent) => {
@ -37,20 +50,15 @@ export const LedgerManager = ({ partyId }: { partyId: string }) => {
setFilter(updatedFilter); setFilter(updatedFilter);
}, []); }, []);
// allow passing undefined to grid so that loading state is shown
const extractedData = data?.map((item) => item.node);
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<LedgerTable <LedgerTable
ref={gridRef} ref={gridRef}
rowData={extractedData} rowData={data}
onFilterChanged={onFilterChanged} onFilterChanged={onFilterChanged}
overlayNoRowsTemplate={error ? error.message : t('No entries')} overlayNoRowsTemplate={error ? error.message : t('No entries')}
/> />
{extractedData && ( {data && <LedgerExportLink entries={data} partyId={partyId} />}
<LedgerExportLink entries={extractedData} partyId={partyId} />
)}
</div> </div>
); );
}; };

View File

@ -1,23 +1,23 @@
import { update } from './order-data-provider'; import {
update,
mapOrderUpdateToOrder,
filterOrderUpdates,
} from './order-data-provider';
import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../'; import type { OrderUpdateFieldsFragment, OrderFieldsFragment } from '../';
import type { Edge } from '@vegaprotocol/data-provider';
describe('order data provider', () => { describe('order data provider', () => {
it('puts incoming data in proper place', () => { it('puts incoming data in proper place', () => {
const data = [ const data = [
{ {
node: { id: '2',
id: '2', createdAt: new Date('2022-01-29').toISOString(),
createdAt: new Date('2022-01-29').toISOString(),
},
}, },
{ {
node: { id: '1',
id: '1', createdAt: new Date('2022-01-28').toISOString(),
createdAt: new Date('2022-01-28').toISOString(),
},
}, },
] as Edge<OrderFieldsFragment>[]; ] as OrderFieldsFragment[];
const delta = [ const delta = [
// this one should be dropped because id don't exits and it's older than newest // this one should be dropped because id don't exits and it's older than newest
@ -52,39 +52,41 @@ describe('order data provider', () => {
createdAt: new Date('2022-02-05').toISOString(), createdAt: new Date('2022-02-05').toISOString(),
}, },
] as OrderUpdateFieldsFragment[]; ] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, { partyId: '0x123' }); const updatedData = update(
data,
filterOrderUpdates(delta),
{ partyId: '0x123' },
mapOrderUpdateToOrder
);
expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual(
-1
);
expect(updatedData && updatedData[3].id).toEqual(delta[2].id);
expect(updatedData && updatedData[3].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[0].id).toEqual(delta[5].id);
expect(updatedData && updatedData[1].id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[4].updatedAt);
expect( expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id) update(
).toEqual(-1); [],
expect(updatedData && updatedData[3].node.id).toEqual(delta[2].id); filterOrderUpdates(delta),
expect(updatedData && updatedData[3].node.updatedAt).toEqual( { partyId: '0x123' },
delta[2].updatedAt mapOrderUpdateToOrder
); )?.length
expect(updatedData && updatedData[0].node.id).toEqual(delta[5].id); ).toEqual(5);
expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id);
expect(updatedData && updatedData[2].node.id).toEqual(delta[4].id);
expect(updatedData && updatedData[2].node.updatedAt).toEqual(
delta[4].updatedAt
);
expect(update([], delta, () => null, { partyId: '0x123' })?.length).toEqual(
5
);
}); });
it('add only data matching date range filter', () => { it('add only data matching date range filter', () => {
const data = [ const data = [
{ {
node: { id: '1',
id: '1', createdAt: new Date('2022-01-29').toISOString(),
createdAt: new Date('2022-01-29').toISOString(),
},
}, },
{ {
node: { id: '2',
id: '2', createdAt: new Date('2022-01-30').toISOString(),
createdAt: new Date('2022-01-30').toISOString(),
},
}, },
] as Edge<OrderFieldsFragment>[]; ] as OrderFieldsFragment[];
const delta = [ const delta = [
// this one should be ignored because it does not match date range // this one should be ignored because it does not match date range
@ -105,22 +107,23 @@ describe('order data provider', () => {
}, },
] as OrderUpdateFieldsFragment[]; ] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, { const updatedData = update(
partyId: '0x123', data,
filter: { filterOrderUpdates(delta),
dateRange: { end: new Date('2022-02-01').toISOString() }, {
partyId: '0x123',
filter: {
dateRange: { end: new Date('2022-02-01').toISOString() },
},
}, },
}); mapOrderUpdateToOrder
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].node.updatedAt).toEqual(
delta[2].updatedAt
); );
expect(updatedData && updatedData[2].node.id).toEqual(delta[1].id); expect(updatedData?.findIndex((node) => node.id === delta[0].id)).toEqual(
expect(updatedData && updatedData[2].node.updatedAt).toEqual( -1
delta[1].updatedAt
); );
expect(updatedData && updatedData[0].id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].updatedAt).toEqual(delta[2].updatedAt);
expect(updatedData && updatedData[2].id).toEqual(delta[1].id);
expect(updatedData && updatedData[2].updatedAt).toEqual(delta[1].updatedAt);
}); });
}); });

View File

@ -6,8 +6,8 @@ import {
defaultAppend as append, defaultAppend as append,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets'; import { marketsMapProvider } from '@vegaprotocol/markets';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider'; import type { PageInfo, Edge, Cursor } from '@vegaprotocol/data-provider';
import { OrderStatus } from '@vegaprotocol/types'; import { OrderStatus } from '@vegaprotocol/types';
import type { import type {
OrderFieldsFragment, OrderFieldsFragment,
@ -15,6 +15,7 @@ import type {
OrdersQuery, OrdersQuery,
OrdersUpdateSubscription, OrdersUpdateSubscription,
OrdersQueryVariables, OrdersQueryVariables,
OrdersUpdateSubscriptionVariables,
} from './__generated__/Orders'; } from './__generated__/Orders';
import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders'; import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
import type { ApolloClient } from '@apollo/client'; import type { ApolloClient } from '@apollo/client';
@ -37,18 +38,21 @@ const orderMatchFilters = (
if (!order) { if (!order) {
return true; return true;
} }
if ( if (
variables?.filter?.status && variables?.filter?.status &&
!(order.status && variables.filter.status.includes(order.status)) !(order.status && variables.filter.status.includes(order.status))
) { ) {
return false; return false;
} }
if ( if (
variables?.filter?.liveOnly && variables?.filter?.liveOnly &&
!(order.status && liveOnlyOrderStatuses.includes(order.status)) !(order.status && liveOnlyOrderStatuses.includes(order.status))
) { ) {
return false; return false;
} }
if ( if (
variables?.filter?.types && variables?.filter?.types &&
!(order.type && variables.filter.types.includes(order.type)) !(order.type && variables.filter.types.includes(order.type))
@ -76,10 +80,11 @@ const orderMatchFilters = (
) { ) {
return false; return false;
} }
return true; return true;
}; };
const mapOrderUpdateToOrder = ( export const mapOrderUpdateToOrder = (
orderUpdate: OrderUpdateFieldsFragment orderUpdate: OrderUpdateFieldsFragment
): OrderFieldsFragment => { ): OrderFieldsFragment => {
const { marketId, liquidityProvisionId, ...order } = orderUpdate; const { marketId, liquidityProvisionId, ...order } = orderUpdate;
@ -101,10 +106,36 @@ const mapOrderUpdateToOrder = (
}; };
}; };
const mapOrderUpdateToOrderWithMarket =
(markets: Record<string, Market>) =>
(orderUpdate: OrderUpdateFieldsFragment): Order => {
const { market, ...order } = mapOrderUpdateToOrder(orderUpdate);
return {
...order,
market: markets[market.id],
};
};
const getData = ( const getData = (
responseData: OrdersQuery | null responseData: OrdersQuery | null
): Edge<OrderFieldsFragment>[] => ): (OrderFieldsFragment & Cursor)[] =>
responseData?.party?.ordersConnection?.edges || []; responseData?.party?.ordersConnection?.edges?.map<
OrderFieldsFragment & Cursor
>((edge) => ({ ...edge.node, cursor: edge.cursor })) || [];
export const filterOrderUpdates = (
orders: OrdersUpdateSubscription['orders']
) => {
// A single update can contain the same order with multiple updates, so we need to find
// the latest version of the order and only update using that
return orderBy(
uniqBy(
orderBy(orders, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
};
const getDelta = ( const getDelta = (
subscriptionData: OrdersUpdateSubscription, subscriptionData: OrdersUpdateSubscription,
@ -114,49 +145,32 @@ const getDelta = (
if (!subscriptionData.orders) { if (!subscriptionData.orders) {
return []; return [];
} }
return subscriptionData.orders; return filterOrderUpdates(subscriptionData.orders);
}; };
export const update = ( export const update = <T extends Omit<OrderFieldsFragment, 'market'> & Cursor>(
data: ReturnType<typeof getData> | null, data: T[] | null,
delta: ReturnType<typeof getDelta>, delta: ReturnType<typeof getDelta>,
reload: () => void, variables: OrdersQueryVariables,
variables?: OrdersQueryVariables mapDeltaToData: (delta: OrderUpdateFieldsFragment) => T
) => { ): T[] => {
if (!data) { const updatedData = data ? [...data] : ([] as T[]);
return data; delta.forEach((orderUpdate) => {
} const index = data?.findIndex((order) => order.id === orderUpdate.id) ?? -1;
// A single update can contain the same order with multiple updates, so we need to find const newer = !data?.length || orderUpdate.createdAt >= data[0].createdAt;
// the latest version of the order and only update using that
const incoming = orderBy(
uniqBy(
orderBy(delta, (order) => order.updatedAt || order.createdAt, 'desc'),
'id'
),
'createdAt'
);
const updatedData = [...data];
incoming.forEach((orderUpdate) => {
const index = data.findIndex((edge) => edge.node.id === orderUpdate.id);
const newer =
data.length === 0 || orderUpdate.createdAt >= data[0].node.createdAt;
const doesFilterPass = const doesFilterPass =
!variables || orderMatchFilters(orderUpdate, variables); !variables || orderMatchFilters(orderUpdate, variables);
if (index !== -1) { if (index !== -1) {
if (doesFilterPass) { if (doesFilterPass) {
updatedData[index] = { updatedData[index] = {
...updatedData[index], ...updatedData[index],
node: mapOrderUpdateToOrder(orderUpdate), ...mapDeltaToData(orderUpdate),
}; };
} else { } else {
updatedData.splice(index, 1); updatedData.splice(index, 1);
} }
} else if (newer && doesFilterPass) { } else if (newer && doesFilterPass) {
updatedData.unshift({ updatedData.unshift(mapDeltaToData(orderUpdate));
node: mapOrderUpdateToOrder(orderUpdate),
cursor: '',
});
} }
}); });
return updatedData; return updatedData;
@ -170,11 +184,13 @@ export const ordersProvider = makeDataProvider<
ReturnType<typeof getData>, ReturnType<typeof getData>,
OrdersUpdateSubscription, OrdersUpdateSubscription,
ReturnType<typeof getDelta>, ReturnType<typeof getDelta>,
OrdersQueryVariables OrdersQueryVariables,
OrdersUpdateSubscriptionVariables
>({ >({
query: OrdersDocument, query: OrdersDocument,
subscriptionQuery: OrdersUpdateDocument, subscriptionQuery: OrdersUpdateDocument,
update, update: (data, delta, reload, variables) =>
update(data, delta, variables, mapOrderUpdateToOrder),
getData, getData,
getDelta, getDelta,
pagination: { pagination: {
@ -185,6 +201,10 @@ export const ordersProvider = makeDataProvider<
resetDelay: 1000, resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true }, additionalContext: { isEnlargedTimeout: true },
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
getSubscriptionVariables: ({ partyId, marketIds }) => ({
partyId,
marketIds,
}),
}); });
export const activeOrdersProvider = makeDerivedDataProvider< export const activeOrdersProvider = makeDerivedDataProvider<
@ -208,27 +228,36 @@ export const activeOrdersProvider = makeDerivedDataProvider<
} }
const orders = partsData[0] as ReturnType<typeof getData>; const orders = partsData[0] as ReturnType<typeof getData>;
return variables.marketId return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id) ? orders.filter((order) => variables.marketId === order.market.id)
: orders; : orders;
} }
); );
export const ordersWithMarketProvider = makeDerivedDataProvider< export const ordersWithMarketProvider = makeDerivedDataProvider<
(Order | null)[], (Order & Cursor)[],
Order[], never,
OrdersQueryVariables OrdersQueryVariables
>( >(
[ [
ordersProvider, ordersProvider,
(callback, client) => marketsProvider(callback, client, undefined), (callback, client) => marketsMapProvider(callback, client, undefined),
], ],
(partsData): Order[] => (partsData, variables, prevData, parts): Order[] => {
((partsData[0] as ReturnType<typeof getData>) || []).map((edge) => ({ if (prevData && parts[0].isUpdate) {
...edge.node, return update(
market: (partsData[1] as Market[]).find( prevData,
(market) => market.id === edge.node.market.id parts[0].delta,
), variables,
})) mapOrderUpdateToOrderWithMarket(partsData[1] as Record<string, Market>)
);
}
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
(order) => ({
...order,
market: (partsData[1] as Record<string, Market>)[order.market.id],
})
);
}
); );
export const hasActiveOrderProvider = makeDerivedDataProvider< export const hasActiveOrderProvider = makeDerivedDataProvider<
@ -244,7 +273,7 @@ export const hasAmendableOrderProvider = makeDerivedDataProvider<
>([activeOrdersProvider], (parts) => { >([activeOrdersProvider], (parts) => {
const activeOrders = parts[0] as ReturnType<typeof getData>; const activeOrders = parts[0] as ReturnType<typeof getData>;
const hasAmendableOrder = activeOrders.some( const hasAmendableOrder = activeOrders.some(
(edge) => !(edge.node.liquidityProvision || edge.node.peggedOrder) (order) => !(order.liquidityProvision || order.peggedOrder)
); );
return hasAmendableOrder; return hasAmendableOrder;
}); });

View File

@ -32,7 +32,6 @@ describe('OrderListManager', () => {
flush: jest.fn(), flush: jest.fn(),
reload: jest.fn(), reload: jest.fn(),
load: jest.fn(), load: jest.fn(),
totalCount: undefined,
}); });
await act(async () => { await act(async () => {
render(generateJsx()); render(generateJsx());

View File

@ -87,7 +87,7 @@ export const OrderListManager = ({
gridRef.current.api.setRowData(data); gridRef.current.api.setRowData(data);
return true; return true;
} }
return true; return false;
}, },
}); });

View File

@ -10,32 +10,33 @@ fragment TradeFields on Trade {
} }
query Trades($marketId: ID!, $pagination: Pagination) { query Trades($marketId: ID!, $pagination: Pagination) {
market(id: $marketId) { trades(filter: { marketIds: [$marketId] }, pagination: $pagination) {
id edges {
tradesConnection(pagination: $pagination) { node {
edges { ...TradeFields
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
} }
} }
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
subscription TradesUpdate($marketId: ID!) { subscription TradesUpdate($marketId: ID!) {
trades(marketId: $marketId) { tradesStream(filter: { marketIds: [$marketId] }) {
id ...TradeUpdateFields
price
size
createdAt
marketId
aggressor
} }
} }

View File

@ -11,14 +11,16 @@ export type TradesQueryVariables = Types.Exact<{
}>; }>;
export type TradesQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, tradesConnection?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null } | null }; export type TradesQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, price: string, size: string, createdAt: any, aggressor: Types.Side, market: { __typename?: 'Market', id: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
export type TradeUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side };
export type TradesUpdateSubscriptionVariables = Types.Exact<{ export type TradesUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID']; marketId: Types.Scalars['ID'];
}>; }>;
export type TradesUpdateSubscription = { __typename?: 'Subscription', trades?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null }; export type TradesUpdateSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, price: string, size: string, createdAt: any, marketId: string, aggressor: Types.Side }> | null };
export const TradeFieldsFragmentDoc = gql` export const TradeFieldsFragmentDoc = gql`
fragment TradeFields on Trade { fragment TradeFields on Trade {
@ -32,23 +34,30 @@ export const TradeFieldsFragmentDoc = gql`
} }
} }
`; `;
export const TradeUpdateFieldsFragmentDoc = gql`
fragment TradeUpdateFields on TradeUpdate {
id
price
size
createdAt
marketId
aggressor
}
`;
export const TradesDocument = gql` export const TradesDocument = gql`
query Trades($marketId: ID!, $pagination: Pagination) { query Trades($marketId: ID!, $pagination: Pagination) {
market(id: $marketId) { trades(filter: {marketIds: [$marketId]}, pagination: $pagination) {
id edges {
tradesConnection(pagination: $pagination) { node {
edges { ...TradeFields
node {
...TradeFields
}
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
cursor
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
} }
} }
} }
@ -84,16 +93,11 @@ export type TradesLazyQueryHookResult = ReturnType<typeof useTradesLazyQuery>;
export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>; export type TradesQueryResult = Apollo.QueryResult<TradesQuery, TradesQueryVariables>;
export const TradesUpdateDocument = gql` export const TradesUpdateDocument = gql`
subscription TradesUpdate($marketId: ID!) { subscription TradesUpdate($marketId: ID!) {
trades(marketId: $marketId) { tradesStream(filter: {marketIds: [$marketId]}) {
id ...TradeUpdateFields
price
size
createdAt
marketId
aggressor
} }
} }
`; ${TradeUpdateFieldsFragmentDoc}`;
/** /**
* __useTradesUpdateSubscription__ * __useTradesUpdateSubscription__

View File

@ -1,4 +1,3 @@
import compact from 'lodash/compact';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react'; import { useRef } from 'react';
@ -19,12 +18,11 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
dataProvider: tradesWithMarketProvider, dataProvider: tradesWithMarketProvider,
variables: { marketId }, variables: { marketId },
}); });
const trades = compact(data).map((d) => d.node);
return ( return (
<TradesTable <TradesTable
ref={gridRef} ref={gridRef}
rowData={trades} rowData={data}
onClick={(price?: string) => { onClick={(price?: string) => {
if (price) { if (price) {
updateOrder(marketId, { price }); updateOrder(marketId, { price });

View File

@ -3,75 +3,95 @@ import {
makeDerivedDataProvider, makeDerivedDataProvider,
defaultAppend as append, defaultAppend as append,
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider'; import type { PageInfo, Cursor } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets'; import { marketsMapProvider } from '@vegaprotocol/markets';
import type { import type {
TradesQuery, TradesQuery,
TradesQueryVariables, TradesQueryVariables,
TradeFieldsFragment, TradeFieldsFragment,
TradesUpdateSubscription, TradesUpdateSubscription,
TradeUpdateFieldsFragment,
TradesUpdateSubscriptionVariables,
} from './__generated__/Trades'; } from './__generated__/Trades';
import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades'; import { TradesDocument, TradesUpdateDocument } from './__generated__/Trades';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import produce from 'immer';
export const MAX_TRADES = 500; export const MAX_TRADES = 500;
const getData = ( const getData = (
responseData: TradesQuery | null responseData: TradesQuery | null
): ({ ): (TradeFieldsFragment & Cursor)[] =>
cursor: string; responseData?.trades?.edges.map<TradeFieldsFragment & Cursor>((edge) => ({
node: TradeFieldsFragment; ...edge.node,
} | null)[] => responseData?.market?.tradesConnection?.edges || []; cursor: edge.cursor,
})) || [];
const getDelta = (subscriptionData: TradesUpdateSubscription) => const getDelta = (subscriptionData: TradesUpdateSubscription) =>
subscriptionData?.trades || []; subscriptionData?.tradesStream || [];
const update = ( const mapTradeUpdateToTrade = (
data: ReturnType<typeof getData> | null, tradeUpdate: TradeUpdateFieldsFragment
delta: ReturnType<typeof getDelta> ): TradeFieldsFragment => {
) => { const { marketId, ...trade } = tradeUpdate;
if (!data) return data; return {
return produce(data, (draft) => { ...trade,
// for each incoming trade add it to the beginning and remove oldest trade __typename: 'Trade',
orderBy(delta, 'createdAt', 'desc').forEach((node) => { market: {
const { marketId, ...nodeData } = node; __typename: 'Market',
draft.unshift({ id: marketId,
node: { },
...nodeData, };
__typename: 'Trade', };
market: {
__typename: 'Market',
id: marketId,
},
},
cursor: '',
});
if (draft.length > MAX_TRADES) { const mapTradeUpdateToTradeWithMarket =
draft.pop(); (markets: Record<string, Market>) =>
} (tradeUpdate: TradeUpdateFieldsFragment): Trade => {
}); const { market, ...trade } = mapTradeUpdateToTrade(tradeUpdate);
return {
...trade,
market: markets[market.id],
};
};
const update = <T extends Omit<TradeFieldsFragment, 'market'> & Cursor>(
data: T[] | null,
delta: ReturnType<typeof getDelta>,
variables: TradesQueryVariables,
mapDeltaToData: (delta: TradeUpdateFieldsFragment) => T
): T[] => {
const updatedData = data ? [...data] : ([] as T[]);
orderBy(delta, 'createdAt', 'desc').forEach((tradeUpdate) => {
const index = data?.findIndex((trade) => trade.id === tradeUpdate.id) ?? -1;
if (index !== -1) {
updatedData[index] = {
...updatedData[index],
...mapDeltaToData(tradeUpdate),
};
} else if (!data?.length || tradeUpdate.createdAt >= data[0].createdAt) {
updatedData.unshift(mapDeltaToData(tradeUpdate));
}
}); });
return updatedData.slice(0, MAX_TRADES);
}; };
export type Trade = Omit<TradeFieldsFragment, 'market'> & { market?: Market }; export type Trade = Omit<TradeFieldsFragment, 'market'> & { market?: Market };
export type TradeEdge = Edge<Trade>;
const getPageInfo = (responseData: TradesQuery | null): PageInfo | null => const getPageInfo = (responseData: TradesQuery | null): PageInfo | null =>
responseData?.market?.tradesConnection?.pageInfo || null; responseData?.trades?.pageInfo || null;
export const tradesProvider = makeDataProvider< export const tradesProvider = makeDataProvider<
Parameters<typeof getData>['0'], Parameters<typeof getData>['0'],
ReturnType<typeof getData>, ReturnType<typeof getData>,
Parameters<typeof getDelta>['0'], Parameters<typeof getDelta>['0'],
ReturnType<typeof getDelta>, ReturnType<typeof getDelta>,
TradesQueryVariables TradesQueryVariables,
TradesUpdateSubscriptionVariables
>({ >({
query: TradesDocument, query: TradesDocument,
subscriptionQuery: TradesUpdateDocument, subscriptionQuery: TradesUpdateDocument,
update, update: (data, delta, reload, variables) =>
update(data, delta, variables, mapTradeUpdateToTrade),
getData, getData,
getDelta, getDelta,
pagination: { pagination: {
@ -79,34 +99,32 @@ export const tradesProvider = makeDataProvider<
append, append,
first: MAX_TRADES, first: MAX_TRADES,
}, },
getSubscriptionVariables: ({ marketId }) => ({ marketId }),
}); });
export const tradesWithMarketProvider = makeDerivedDataProvider< export const tradesWithMarketProvider = makeDerivedDataProvider<
(TradeEdge | null)[], (Trade & Cursor)[],
Trade[], never,
TradesQueryVariables TradesQueryVariables
>( >(
[ [
tradesProvider, tradesProvider,
(callback, client) => marketsProvider(callback, client, undefined), (callback, client) => marketsMapProvider(callback, client, undefined),
], ],
(partsData): (TradeEdge | null)[] | null => { (partsData, variables, prevData, parts): Trade[] | null => {
const edges = partsData[0] as ReturnType<typeof getData>; if (prevData && parts[0].isUpdate) {
return edges.map((edge) => { return update(
if (edge === null) { prevData,
return null; parts[0].delta as ReturnType<typeof getDelta>,
} variables,
const node = { mapTradeUpdateToTradeWithMarket(partsData[1] as Record<string, Market>)
...edge.node, );
market: (partsData[1] as Market[]).find( }
(market) => market.id === edge.node.market.id return ((partsData[0] as ReturnType<typeof getData>) || []).map(
), (trade) => ({
}; ...trade,
const cursor = edge?.cursor || ''; market: (partsData[1] as Record<string, Market>)[trade.market.id],
return { })
cursor, );
node,
};
});
} }
); );

View File

@ -11,24 +11,20 @@ export const tradesQuery = (
override?: PartialDeep<TradesQuery> override?: PartialDeep<TradesQuery>
): TradesQuery => { ): TradesQuery => {
const defaultResult: TradesQuery = { const defaultResult: TradesQuery = {
market: { trades: {
id: 'market-0', __typename: 'TradeConnection',
tradesConnection: { edges: trades.map((node, i) => ({
__typename: 'TradeConnection', __typename: 'TradeEdge',
edges: trades.map((node, i) => ({ node,
__typename: 'TradeEdge', cursor: (i + 1).toString(),
node, })),
cursor: (i + 1).toString(), pageInfo: {
})), __typename: 'PageInfo',
pageInfo: { startCursor: '0',
__typename: 'PageInfo', endCursor: trades.length.toString(),
startCursor: '0', hasNextPage: false,
endCursor: trades.length.toString(), hasPreviousPage: false,
hasNextPage: false,
hasPreviousPage: false,
},
}, },
__typename: 'Market',
}, },
}; };
@ -40,7 +36,7 @@ export const tradesUpdateSubscription = (
): TradesUpdateSubscription => { ): TradesUpdateSubscription => {
const defaultResult: TradesUpdateSubscription = { const defaultResult: TradesUpdateSubscription = {
__typename: 'Subscription', __typename: 'Subscription',
trades: [ tradesStream: [
{ {
__typename: 'TradeUpdate', __typename: 'TradeUpdate',
id: '1234567890', id: '1234567890',