From a6576132b1e2f7296a2163a687b1a6f7d8285197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Wed, 19 Oct 2022 11:14:18 +0200 Subject: [PATCH] chore: generic data provider improvments (#1772) --- libs/fills/src/lib/fills-data-provider.ts | 27 +--- .../order-data-provider.ts | 29 +--- .../src/lib/generic-data-provider.spec.ts | 138 ++++++++++++++---- .../src/lib/generic-data-provider.ts | 83 +++++++---- libs/react-helpers/src/lib/get-nodes.ts | 8 +- libs/react-helpers/src/lib/pagination.ts | 39 ++++- libs/trades/src/lib/trades-data-provider.ts | 27 +--- 7 files changed, 227 insertions(+), 124 deletions(-) diff --git a/libs/fills/src/lib/fills-data-provider.ts b/libs/fills/src/lib/fills-data-provider.ts index 2a2218de5..8dff7d1b1 100644 --- a/libs/fills/src/lib/fills-data-provider.ts +++ b/libs/fills/src/lib/fills-data-provider.ts @@ -4,10 +4,12 @@ import { makeDataProvider, makeDerivedDataProvider, defaultAppend as append, + paginatedCombineDelta as combineDelta, + paginatedCombineInsertionData as combineInsertionData, } from '@vegaprotocol/react-helpers'; import type { Market } from '@vegaprotocol/market-list'; import { marketsProvider } from '@vegaprotocol/market-list'; -import type { PageInfo } from '@vegaprotocol/react-helpers'; +import type { PageInfo, Edge } from '@vegaprotocol/react-helpers'; import { FillsDocument, FillsEventDocument } from './__generated___/Fills'; import type { FillsQuery, @@ -55,10 +57,7 @@ const update = ( }; export type Trade = Omit & { market?: Market }; -export type TradeEdge = { - cursor: FillEdgeFragment['cursor']; - node: Trade; -}; +export type TradeEdge = Edge; const getData = (responseData: FillsQuery): FillEdgeFragment[] => responseData.party?.tradesConnection?.edges || []; @@ -100,20 +99,6 @@ export const fillsWithMarketProvider = makeDerivedDataProvider< }, } ) || null, - (parts): Trade[] | undefined => { - if (!parts[0].isUpdate) { - return; - } - // map FillsSub_trades[] from subscription to updated Trade[] - return (parts[0].delta as ReturnType).map( - (deltaTrade) => ({ - ...((parts[0].data as ReturnType)?.find( - (trade) => trade.node.id === deltaTrade.id - )?.node as FillFieldsFragment), - market: (parts[1].data as Market[]).find( - (market) => market.id === deltaTrade.marketId - ), - }) - ); - } + combineDelta['0']>, + combineInsertionData ); diff --git a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts index e0a409358..be5e8ea75 100644 --- a/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts +++ b/libs/orders/src/lib/components/order-data-provider/order-data-provider.ts @@ -6,10 +6,12 @@ import { makeDataProvider, makeDerivedDataProvider, defaultAppend as append, + paginatedCombineDelta as combineDelta, + paginatedCombineInsertionData as combineInsertionData, } from '@vegaprotocol/react-helpers'; import type { Market } from '@vegaprotocol/market-list'; import { marketsProvider } from '@vegaprotocol/market-list'; -import type { PageInfo } from '@vegaprotocol/react-helpers'; +import type { PageInfo, Edge } from '@vegaprotocol/react-helpers'; import type { Orders, Orders_party_ordersConnection_edges, @@ -140,10 +142,7 @@ export const update = ( export type Order = Omit & { market?: Market; }; -export type OrderEdge = { - node: Order; - cursor: Orders_party_ordersConnection_edges['cursor']; -}; +export type OrderEdge = Edge; const getData = ( responseData: Orders @@ -169,7 +168,7 @@ export const ordersProvider = makeDataProvider({ }); export const ordersWithMarketProvider = makeDerivedDataProvider< - OrderEdge[], + (OrderEdge | null)[], Order[] >( [ordersProvider, marketsProvider], @@ -183,20 +182,6 @@ export const ordersWithMarketProvider = makeDerivedDataProvider< ), }, })), - (parts): Order[] | undefined => { - if (!parts[0].isUpdate) { - return; - } - // map OrderSub_orders[] from subscription to updated Order[] - return (parts[0].delta as ReturnType).map( - (deltaOrder) => ({ - ...((parts[0].data as ReturnType)?.find( - (order) => order.node.id === deltaOrder.id - )?.node as Orders_party_ordersConnection_edges_node), - market: (parts[1].data as Market[]).find( - (market) => market.id === deltaOrder.marketId - ), - }) - ); - } + combineDelta['0']>, + combineInsertionData ); diff --git a/libs/react-helpers/src/lib/generic-data-provider.spec.ts b/libs/react-helpers/src/lib/generic-data-provider.spec.ts index 951b8d060..b1d532298 100644 --- a/libs/react-helpers/src/lib/generic-data-provider.spec.ts +++ b/libs/react-helpers/src/lib/generic-data-provider.spec.ts @@ -6,6 +6,7 @@ import { import type { CombineDerivedData, CombineDerivedDelta, + CombineInsertionData, Query, UpdateCallback, Update, @@ -65,34 +66,20 @@ const subscribe = makeDataProvider({ getDelta: (r) => r.data, }); -const secondSubscribe = makeDataProvider< - QueryData, - Data, - SubscriptionData, - Delta ->({ - query, - subscriptionQuery, - update, - getData: (r) => r.data, - getDelta: (r) => r.data, -}); - const combineData = jest.fn< ReturnType>, Parameters> >(); const combineDelta = jest.fn< - ReturnType>, - Parameters> + ReturnType>, + Parameters> >(); -const derivedSubscribe = makeDerivedDataProvider( - [subscribe, secondSubscribe], - combineData, - combineDelta -); +const combineInsertionData = jest.fn< + ReturnType>, + Parameters> +>(); const first = 100; const paginatedSubscribe = makeDataProvider< @@ -114,6 +101,13 @@ const paginatedSubscribe = makeDataProvider< }, }); +const derivedSubscribe = makeDerivedDataProvider( + [paginatedSubscribe, subscribe], + combineData, + combineDelta, + combineInsertionData +); + const generateData = (start = 0, size = first) => { return new Array(size).fill(null).map((v, i) => ({ cursor: (i + start + 1).toString(), @@ -514,22 +508,30 @@ describe('derived data provider', () => { it('calls callback on each meaningful update, uses combineData function', async () => { clearPendingQueries(); - const part1: Item[] = []; + const totalCount = 1000; + const part1 = { + data: generateData(), + totalCount, + pageInfo: { + hasNextPage: true, + endCursor: '100', + }, + }; const part2: Item[] = []; const callback = jest.fn< - ReturnType>, - Parameters> + ReturnType>, + Parameters> >(); const subscription = derivedSubscribe(callback, client); const data = { totalCount: 0 }; combineData.mockReturnValueOnce(data); expect(callback.mock.calls.length).toBe(0); - await resolveQuery({ data: part1 }); + await resolveQuery(part1); expect(combineData.mock.calls.length).toBe(0); expect(callback.mock.calls.length).toBe(0); await resolveQuery({ data: part2 }); expect(combineData.mock.calls.length).toBe(1); - expect(combineData.mock.calls[0][0][0]).toBe(part1); + expect(combineData.mock.calls[0][0][0]).toBe(part1.data); expect(combineData.mock.calls[0][0][1]).toBe(part2); expect(callback.mock.calls.length).toBe(1); expect(callback.mock.calls[0][0].data).toBe(data); @@ -538,13 +540,15 @@ describe('derived data provider', () => { }); it('callback with error if any dependency has error, reloads all dependencies on reload', async () => { + clearPendingQueries(); combineData.mockClear(); const part1: Item[] = []; const part2: Item[] = []; const callback = jest.fn< - ReturnType>, - Parameters> + ReturnType>, + Parameters> >(); + expect(callback.mock.calls.length).toBe(0); const subscription = derivedSubscribe(callback, client); const data = { totalCount: 0 }; combineData.mockReturnValueOnce(data); @@ -570,4 +574,84 @@ describe('derived data provider', () => { expect(callback.mock.calls[2][0].error).toBeUndefined(); subscription.unsubscribe(); }); + + it('pass isUpdate on any dependency isUpdate, uses result of combineDelta as delta in next callback', async () => { + clearPendingQueries(); + combineData.mockClear(); + const part1: Item[] = []; + const part2: Item[] = []; + const callback = jest.fn< + ReturnType>, + Parameters> + >(); + const subscription = derivedSubscribe(callback, client); + const data = { totalCount: 0 }; + combineData.mockReturnValueOnce(data); + await resolveQuery({ data: part1 }); + await resolveQuery({ data: part2 }); + expect(combineData).toBeCalledTimes(1); + expect(callback).toBeCalledTimes(1); + update.mockImplementation((data, delta) => [...data, ...delta]); + combineData.mockReturnValueOnce({ ...data }); + const combinedDelta = {}; + combineDelta.mockReturnValueOnce(combinedDelta); + // calling onNext from client.subscribe({ query }).subscribe(onNext) + const delta: Item[] = []; + await clientSubscribeSubscribe.mock.calls[ + clientSubscribeSubscribe.mock.calls.length - 1 + ][0]({ data: { data: delta } }); + expect(combineDelta).toBeCalledTimes(1); + expect(combineData).toBeCalledTimes(2); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[1][0].isUpdate).toBe(true); + expect(callback.mock.calls[1][0].delta).toBe(combinedDelta); + subscription.unsubscribe(); + }); + + it('pass isInsert on any dependency isInsert, uses result of combineInsertionData as insertionData in next callback', async () => { + clearPendingQueries(); + combineData.mockClear(); + combineDelta.mockClear(); + const callback = jest.fn< + ReturnType>, + Parameters> + >(); + const subscription = derivedSubscribe(callback, client); + const data = { totalCount: 0 }; + combineData.mockReturnValueOnce(data); + await resolveQuery({ + data: generateData(), + pageInfo: { + hasNextPage: true, + endCursor: '100', + }, + }); + await resolveQuery({ data: [] }); + expect(combineData).toBeCalledTimes(1); + expect(callback).toBeCalledTimes(1); + update.mockImplementation((data, delta) => [...data, ...delta]); + combineData.mockReturnValueOnce({ ...data }); + const combinedInsertionData = {}; + combineInsertionData.mockReturnValueOnce(combinedInsertionData); + subscription.load && subscription.load(); + const lastQueryArgs = + clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0]; + expect(lastQueryArgs?.variables?.pagination).toEqual({ + after: '100', + first, + }); + await resolveQuery({ + data: generateData(100), + pageInfo: { + hasNextPage: true, + endCursor: '200', + }, + }); + expect(combineInsertionData).toBeCalledTimes(1); + expect(combineData).toBeCalledTimes(2); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[1][0].isInsert).toBe(true); + expect(callback.mock.calls[1][0].insertionData).toBe(combinedInsertionData); + subscription.unsubscribe(); + }); }); diff --git a/libs/react-helpers/src/lib/generic-data-provider.ts b/libs/react-helpers/src/lib/generic-data-provider.ts index cc67c2ea5..f99898bdc 100644 --- a/libs/react-helpers/src/lib/generic-data-provider.ts +++ b/libs/react-helpers/src/lib/generic-data-provider.ts @@ -94,6 +94,14 @@ interface GetDelta { (subscriptionData: SubscriptionData, variables?: OperationVariables): Delta; } +export type Node = { id: string }; +export type Cursor = { + cursor?: string | null; +}; +export interface Edge extends Cursor { + node: T; +} + export function defaultAppend( data: Data | null, insertionData: Data | null, @@ -104,7 +112,7 @@ export function defaultAppend( if (data && insertionData && insertionPageInfo) { if (!(data instanceof Array) || !(insertionData instanceof Array)) { throw new Error( - 'data needs to be instance of { cursor: string }[] when using pagination' + 'data needs to be instance of Edge[] when using pagination' ); } if (pagination?.after) { @@ -220,9 +228,7 @@ function makeDataProviderInternal({ if (!start) { paginationVariables.after = undefined; } else if (data && data[start - 1]) { - paginationVariables.after = ( - data[start - 1] as { cursor: string } - ).cursor; + paginationVariables.after = (data[start - 1] as Cursor).cursor; } else { let skip = 1; while (!data[start - 1 - skip] && skip <= start) { @@ -232,9 +238,7 @@ function makeDataProviderInternal({ if (skip === start) { paginationVariables.after = undefined; } else { - paginationVariables.after = ( - data[start - 1 - skip] as { cursor: string } - ).cursor; + paginationVariables.after = (data[start - 1 - skip] as Cursor).cursor; } } } else if (!pageInfo.hasNextPage) { @@ -282,7 +286,7 @@ function makeDataProviderInternal({ if (data && pagination) { if (!(data instanceof Array)) { throw new Error( - 'data needs to be instance of { cursor: string }[] when using pagination' + 'data needs to be instance of Edge[] when using pagination' ); } pageInfo = pagination.getPageInfo(res.data); @@ -493,26 +497,35 @@ export function makeDataProvider( */ type DependencySubscribe = Subscribe; // eslint-disable-line @typescript-eslint/no-explicit-any type DependencyUpdateCallback = Parameters['0']; +export type DerivedPart = Parameters['0']; export type CombineDerivedData = ( - data: Parameters['0']['data'][], + data: DerivedPart['data'][], variables?: OperationVariables ) => Data | null; -export type CombineDerivedDelta = ( - parts: Parameters['0'][], +export type CombineDerivedDelta = ( + data: Data, + parts: DerivedPart[], variables?: OperationVariables ) => Delta | undefined; +export type CombineInsertionData = ( + data: Data, + parts: DerivedPart[], + variables?: OperationVariables +) => Data | undefined; + function makeDerivedDataProviderInternal( dependencies: DependencySubscribe[], combineData: CombineDerivedData, - combineDelta?: CombineDerivedDelta + combineDelta?: CombineDerivedDelta, + combineInsertionData?: CombineInsertionData ): Subscribe { let subscriptions: ReturnType[] | undefined; let client: ApolloClient; const callbacks: UpdateCallback[] = []; let variables: OperationVariables | undefined; - const parts: Parameters['0'][] = []; + const parts: DerivedPart[] = []; let data: Data | null = null; let error: Error | undefined; let loading = true; @@ -541,7 +554,8 @@ function makeDerivedDataProviderInternal( const combine = (updatedPartIndex: number) => { let delta: Delta | undefined; let isUpdate = false; - const isInsert = false; + let isInsert = false; + let insertionData: Data | undefined; let newError: Error | undefined; let newLoading = false; let newLoaded = true; @@ -558,17 +572,6 @@ function makeDerivedDataProviderInternal( variables ) : data; - if (newLoaded) { - const updatedPart = parts[updatedPartIndex]; - if (updatedPart.isUpdate) { - isUpdate = true; - if (updatedPart.delta && combineDelta) { - delta = combineDelta(parts, variables); - } - delete updatedPart.isUpdate; - delete updatedPart.delta; - } - } if ( newLoading !== loading || newError !== error || @@ -579,10 +582,30 @@ function makeDerivedDataProviderInternal( error = newError; loaded = newLoaded; data = newData; + if (newLoaded) { + const updatedPart = parts[updatedPartIndex]; + if (updatedPart.isUpdate) { + isUpdate = true; + if (updatedPart.delta && combineDelta && data) { + delta = combineDelta(data, parts, variables); + } + delete updatedPart.isUpdate; + delete updatedPart.delta; + } + if (updatedPart.isInsert) { + isInsert = updatedPartIndex === 0; + if (updatedPart.insertionData && combineInsertionData && data) { + insertionData = combineInsertionData(data, parts, variables); + } + delete updatedPart.insertionData; + delete updatedPart.isInsert; + } + } notifyAll({ isUpdate, isInsert, delta, + insertionData, }); } }; @@ -641,10 +664,16 @@ function makeDerivedDataProviderInternal( export function makeDerivedDataProvider( dependencies: DependencySubscribe[], combineData: CombineDerivedData, - combineDelta?: CombineDerivedDelta + combineDelta?: CombineDerivedDelta, + combineInsertionData?: CombineInsertionData ): Subscribe { const getInstance = memoize(() => - makeDerivedDataProviderInternal(dependencies, combineData, combineDelta) + makeDerivedDataProviderInternal( + dependencies, + combineData, + combineDelta, + combineInsertionData + ) ); return (callback, client, variables) => getInstance(variables)(callback, client, variables); diff --git a/libs/react-helpers/src/lib/get-nodes.ts b/libs/react-helpers/src/lib/get-nodes.ts index 0cb90f1a2..cac3b9026 100644 --- a/libs/react-helpers/src/lib/get-nodes.ts +++ b/libs/react-helpers/src/lib/get-nodes.ts @@ -1,18 +1,16 @@ import type { Schema } from '@vegaprotocol/types'; -export type Node = { - __typename?: string; +type Edge = { node: T; }; -export type Connection = { - __typename?: string; +type Connection = { edges?: Schema.Maybe>>; }; export function getNodes< T, - A extends Node = Node, + A extends Edge = Edge, B extends Connection = Connection >(data?: B | null, filterBy?: (item?: T | null) => boolean) { const edges = data?.edges || []; diff --git a/libs/react-helpers/src/lib/pagination.ts b/libs/react-helpers/src/lib/pagination.ts index a11f52acf..7285261b8 100644 --- a/libs/react-helpers/src/lib/pagination.ts +++ b/libs/react-helpers/src/lib/pagination.ts @@ -1,5 +1,6 @@ import type { IGetRowsParams } from 'ag-grid-community'; -import type { Load } from './generic-data-provider'; +import type { Load, DerivedPart } from './generic-data-provider'; +import type { Node, Edge } from './generic-data-provider'; import type { MutableRefObject } from 'react'; const getLastRow = ( @@ -57,3 +58,39 @@ export const makeInfiniteScrollGetRows = failCallback(); } }; + +export const paginatedCombineDelta = < + DataNode extends Node, + DeltaNode extends Node +>( + data: (Edge | null)[], + parts: DerivedPart[] +): DataNode[] | undefined => { + if (!parts[0].isUpdate) { + return; + } + const updatedIds = (parts[0].delta as DeltaNode[]).map((node) => node.id); + return data + .filter>( + (edge): edge is Edge => + edge !== null && updatedIds.includes(edge.node.id) + ) + .map((edge) => edge.node); +}; + +export const paginatedCombineInsertionData = ( + data: (Edge | null)[], + parts: DerivedPart[] +): Edge[] | undefined => { + if (!parts[0].isInsert) { + return; + } + const insertedIds = (parts[0].insertionData as DataNode[]).map( + (node) => node.id + ); + // get updated orders + return data.filter>( + (edge): edge is Edge => + edge !== null && insertedIds.includes(edge.node.id) + ); +}; diff --git a/libs/trades/src/lib/trades-data-provider.ts b/libs/trades/src/lib/trades-data-provider.ts index 89587e44d..9026786ed 100644 --- a/libs/trades/src/lib/trades-data-provider.ts +++ b/libs/trades/src/lib/trades-data-provider.ts @@ -2,8 +2,10 @@ import { makeDataProvider, makeDerivedDataProvider, defaultAppend as append, + paginatedCombineDelta as combineDelta, + paginatedCombineInsertionData as combineInsertionData, } from '@vegaprotocol/react-helpers'; -import type { PageInfo } from '@vegaprotocol/react-helpers'; +import type { PageInfo, Edge } from '@vegaprotocol/react-helpers'; import type { Market } from '@vegaprotocol/market-list'; import { marketsProvider } from '@vegaprotocol/market-list'; import type { @@ -63,10 +65,7 @@ const update = ( }; export type Trade = Omit & { market?: Market }; -export type TradeEdge = { - cursor: string; - node: Trade; -}; +export type TradeEdge = Edge; const getPageInfo = (responseData: TradesQuery): PageInfo | null => responseData.market?.tradesConnection?.pageInfo || null; @@ -108,20 +107,6 @@ export const tradesWithMarketProvider = makeDerivedDataProvider< }; }); }, - (parts): Trade[] | undefined => { - if (!parts[0].isUpdate) { - return; - } - // map FillsSub_trades[] from subscription to updated Trade[] - return (parts[0].delta as ReturnType).map( - (deltaTrade) => ({ - ...((parts[0].data as ReturnType)?.find( - (edge) => edge?.node.id === deltaTrade.id - )?.node as Trade), - market: (parts[1].data as Market[]).find( - (market) => market.id === deltaTrade.marketId - ), - }) - ); - } + combineDelta['0']>, + combineInsertionData );