chore: set default fetchPolicy, handle subscription errors like query errors, timeout unsubscribe (#1482)

* chore: set default fetchPolicy, handle subscription errors like query errors,add unsubscribe timeout

* chore: improve no data handling in fills, ordes and trades

* chore: make reset delay optional, fix pagination and useOrderListData spec
This commit is contained in:
Bartłomiej Głownia 2022-09-27 20:48:53 +02:00 committed by GitHub
parent adf6059701
commit e310f04034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 118 additions and 72 deletions

View File

@ -15,19 +15,30 @@ import type {
TabToNextCellParams,
CellKeyDownEvent,
FullWidthCellKeyDownEvent,
IGetRowsParams,
IDatasource,
} from 'ag-grid-community';
import { NO_DATA_MESSAGE } from '../../constants';
import * as constants from '../simple-market-list/constants';
export interface GetRowsParams<T>
extends Omit<IGetRowsParams, 'successCallback'> {
successCallback(rowsThisBlock: T[], lastRow?: number): void;
}
export interface Datasource<T> extends IDatasource {
getRows(params: GetRowsParams<T>): void;
}
interface Props<T> extends GridOptions {
data?: T[];
rowData?: T[];
datasource?: Datasource<T>;
handleRowClicked?: (event: { data: T }) => void;
components?: Record<string, unknown>;
classNamesParam?: string | string[];
}
const ConsoleLiteGrid = <T extends { id?: string }>(
{ data, handleRowClicked, getRowId, classNamesParam, ...props }: Props<T>,
{ handleRowClicked, getRowId, classNamesParam, ...props }: Props<T>,
ref?: React.Ref<AgGridReact>
) => {
const { isMobile, screenSize } = useScreenDimensions();
@ -70,7 +81,6 @@ const ConsoleLiteGrid = <T extends { id?: string }>(
return (
<AgGrid
className={classNames(classNamesParam)}
rowData={data}
rowHeight={60}
customThemeParams={
theme === 'dark'

View File

@ -44,7 +44,7 @@ const AccountsManager = () => {
noDataMessage={NO_DATA_MESSAGE}
>
<ConsoleLiteGrid<AccountObj>
data={data as AccountObj[]}
rowData={data as AccountObj[]}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
components={{ PriceCell }}

View File

@ -56,7 +56,8 @@ const OrdersManager = () => {
>
<ConsoleLiteGrid<OrderWithMarket>
ref={gridRef}
rowModelType="infinite"
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}

View File

@ -47,7 +47,7 @@ const PositionsAsset = ({ partyId, assetSymbol }: Props) => {
defaultColDef={defaultColDef}
getRowId={getRowId}
rowModelType={data?.length ? 'infinite' : 'clientSide'}
data={data?.length ? undefined : []}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
components={{ PriceFlashCell }}
/>

View File

@ -67,7 +67,7 @@ const SimpleMarketList = () => {
<ConsoleLiteGrid<MarketWithPercentChange>
classNamesParam="mb-32 min-h-[300px]"
columnDefs={columnDefs}
data={localData}
rowData={localData}
defaultColDef={defaultColDef}
handleRowClicked={handleRowClicked}
/>

View File

@ -33,8 +33,9 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
<FillsTable
ref={gridRef}
partyId={partyId}
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
rowModelType="infinite"
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
/>

View File

@ -47,17 +47,21 @@ export const useFillsList = ({ partyId, gridRef, scrolledToTop }: Props) => {
if (!gridRef.current?.api) {
return false;
}
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += delta.filter(
(trade) => trade.createdAt > createdAt
).length;
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;
}
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
return false;
},
[gridRef, scrolledToTop]
);

View File

@ -83,6 +83,7 @@ export const marketsProvider = makeDataProvider<
>({
query: MARKET_LIST_QUERY,
getData,
fetchPolicy: 'cache-first',
});
export const activeMarketsProvider = makeDerivedDataProvider<Market[], never>(

View File

@ -34,7 +34,8 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
<AsyncRenderer loading={loading} error={error} data={data}>
<OrderList
ref={gridRef}
rowModelType="infinite"
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}

View File

@ -13,7 +13,7 @@ let mockDataProviderData = {
error: undefined,
loading: true,
load: loadMock,
totalCount: 0,
totalCount: undefined,
};
let updateMock: jest.Mock;
@ -98,7 +98,7 @@ describe('useOrderListData Hook', () => {
expect(mockRefreshAgGridApi).toHaveBeenCalled();
});
it('methods for pagination should works', async () => {
it('methods for pagination should work', async () => {
const successCallback = jest.fn();
mockData = [
{
@ -114,12 +114,10 @@ describe('useOrderListData Hook', () => {
},
} as unknown as Orders_party_ordersConnection_edges,
];
mockDataProviderData = {
...mockDataProviderData,
Object.assign(mockDataProviderData, {
data: mockData,
loading: false,
totalCount: 4,
};
});
const mockDelta = [
{
node: {
@ -142,8 +140,6 @@ describe('useOrderListData Hook', () => {
}
);
updateMock({ data: mockNextData, delta: mockDelta });
const getRowsParams = {
successCallback,
failCallback: jest.fn(),
@ -152,12 +148,14 @@ describe('useOrderListData Hook', () => {
} as unknown as IGetRowsParams;
await waitFor(async () => {
await result.current.getRows(getRowsParams);
const promise = result.current.getRows(getRowsParams);
updateMock({ data: mockNextData, delta: mockDelta });
await promise;
});
expect(loadMock).toHaveBeenCalled();
expect(successCallback).toHaveBeenLastCalledWith(
mockDelta.map((item) => item.node),
4
-1
);
});
});

View File

@ -50,17 +50,21 @@ export const useOrderListData = ({
if (!gridRef.current?.api) {
return false;
}
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += delta.filter(
(trade) => trade.createdAt > createdAt
).length;
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;
}
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
return false;
},
[gridRef, scrolledToTop]
);

View File

@ -24,11 +24,7 @@ import type {
ICellRendererParams,
ValueFormatterParams,
} from 'ag-grid-community';
import type {
AgGridReact,
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { forwardRef, useState } from 'react';
import type { Orders_party_ordersConnection_edges_node } from '../';
@ -40,7 +36,7 @@ import { OrderEditDialog } from './order-edit-dialog';
import type { OrderWithMarket } from '../';
import { OrderFeedback } from '../order-feedback';
type OrderListProps = AgGridReactProps | AgReactUiProps;
type OrderListProps = AgGridReactProps;
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
(props, ref) => {
@ -104,7 +100,7 @@ type OrderListTableValueFormatterParams = Omit<
data: OrderWithMarket | null;
};
type OrderListTableProps = (AgGridReactProps | AgReactUiProps) & {
type OrderListTableProps = AgGridReactProps & {
cancel: (order: OrderWithMarket) => void;
setEditOrder: (order: OrderWithMarket) => void;
};

View File

@ -559,15 +559,15 @@ describe('derived data provider', () => {
expect(callback.mock.calls[0][0].error).toBe(error);
expect(callback.mock.calls[0][0].loading).toBe(false);
subscription.reload();
expect(callback.mock.calls.length).toBe(3);
expect(callback.mock.calls[2][0].loading).toBe(true);
expect(callback.mock.calls.length).toBe(2);
expect(callback.mock.calls[1][0].loading).toBe(true);
await resolveQuery({ data: part1 });
expect(callback.mock.calls.length).toBe(3);
expect(callback.mock.calls.length).toBe(2);
await resolveQuery({ data: part2 });
expect(callback.mock.calls.length).toBe(4);
expect(callback.mock.calls[3][0].data).toStrictEqual(data);
expect(callback.mock.calls[3][0].loading).toBe(false);
expect(callback.mock.calls[3][0].error).toBeUndefined();
expect(callback.mock.calls.length).toBe(3);
expect(callback.mock.calls[2][0].data).toStrictEqual(data);
expect(callback.mock.calls[2][0].loading).toBe(false);
expect(callback.mock.calls[2][0].error).toBeUndefined();
subscription.unsubscribe();
});
});

View File

@ -76,7 +76,7 @@ export interface Append<Data> {
}
interface GetData<QueryData, Data> {
(queryData: QueryData): Data | null;
(queryData: QueryData, variables?: OperationVariables): Data | null;
}
interface GetPageInfo<QueryData> {
@ -88,7 +88,7 @@ interface GetTotalCount<QueryData> {
}
interface GetDelta<SubscriptionData, Delta> {
(subscriptionData: SubscriptionData): Delta;
(subscriptionData: SubscriptionData, variables?: OperationVariables): Delta;
}
export function defaultAppend<Data>(
@ -144,6 +144,7 @@ interface DataProviderParams<QueryData, Data, SubscriptionData, Delta> {
first: number;
};
fetchPolicy?: FetchPolicy;
resetDelay?: number;
}
/**
@ -162,6 +163,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
getDelta,
pagination,
fetchPolicy,
resetDelay,
}: DataProviderParams<QueryData, Data, SubscriptionData, Delta>): Subscribe<
Data,
Delta
@ -171,6 +173,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
// subscription is started before initial query, all deltas that will arrive before initial query response are put on queue
const updateQueue: Delta[] = [];
let resetTimer: ReturnType<typeof setTimeout>;
let variables: OperationVariables | undefined;
let data: Data | null = null;
let error: Error | undefined;
@ -242,7 +245,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
},
fetchPolicy: fetchPolicy || 'no-cache',
});
const insertionData = getData(res.data);
const insertionData = getData(res.data, variables);
const insertionPageInfo = pagination.getPageInfo(res.data);
({ data, totalCount } = pagination.append(
data,
@ -269,10 +272,10 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
variables: pagination
? { ...variables, pagination: { first: pagination.first } }
: variables,
fetchPolicy,
fetchPolicy: fetchPolicy || 'no-cache',
errorPolicy: 'ignore',
});
data = getData(res.data);
data = getData(res.data, variables);
if (data && pagination) {
if (!(data instanceof Array)) {
throw new Error(
@ -332,6 +335,9 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
const initialize = async () => {
if (subscription) {
if (resetTimer) {
clearTimeout(resetTimer);
}
return;
}
loading = true;
@ -352,7 +358,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
if (!subscriptionData) {
return;
}
const delta = getDelta(subscriptionData);
const delta = getDelta(subscriptionData, variables);
if (loading || !data) {
updateQueue.push(delta);
} else {
@ -364,17 +370,25 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
notifyAll({ delta, isUpdate: true });
}
},
() => reload()
(e) => {
error = e as Error;
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
}
notifyAll();
}
);
}
await initialFetch();
};
const reset = () => {
if (subscription) {
subscription.unsubscribe();
subscription = undefined;
if (!subscription) {
return;
}
subscription.unsubscribe();
subscription = undefined;
data = null;
error = undefined;
loading = false;
@ -386,7 +400,11 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>({
const unsubscribe = (callback: UpdateCallback<Data, Delta>) => {
callbacks.splice(callbacks.indexOf(callback), 1);
if (callbacks.length === 0) {
reset();
if (resetDelay) {
resetTimer = setTimeout(reset, resetDelay);
} else {
reset();
}
}
};

View File

@ -37,15 +37,22 @@ export const makeInfiniteScrollGetRows =
startRow += newRows.current;
endRow += newRows.current;
try {
if (data.current && data.current.indexOf(null) < endRow) {
await load();
if (data.current) {
const firstMissingRowIndex = data.current.indexOf(null);
if (
endRow > data.current.length ||
(firstMissingRowIndex !== -1 && firstMissingRowIndex < endRow)
) {
await load();
}
}
const rowsThisBlock = data.current
? data.current.slice(startRow, endRow).map((edge) => edge?.node)
: [];
successCallback(
rowsThisBlock,
getLastRow(startRow, endRow, rowsThisBlock.length, totalCount.current)
getLastRow(startRow, endRow, rowsThisBlock.length, totalCount.current) -
newRows.current
);
} catch (e) {
failCallback();

View File

@ -55,17 +55,21 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
if (!gridRef.current?.api) {
return false;
}
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += delta.filter(
(trade) => trade.createdAt > createdAt
).length;
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;
}
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
}
dataRef.current = data;
gridRef.current.api.refreshInfiniteCache();
return true;
return false;
},
[]
);
@ -115,7 +119,8 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
<AsyncRenderer loading={loading} error={error} data={data}>
<TradesTable
ref={gridRef}
rowModelType="infinite"
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}