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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,6 +47,7 @@ export const useFillsList = ({ partyId, gridRef, scrolledToTop }: Props) => {
if (!gridRef.current?.api) { if (!gridRef.current?.api) {
return false; return false;
} }
if (dataRef.current?.length) {
if (!scrolledToTop.current) { if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt; const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) { if (createdAt) {
@ -58,6 +59,9 @@ export const useFillsList = ({ partyId, gridRef, scrolledToTop }: Props) => {
dataRef.current = data; dataRef.current = data;
gridRef.current.api.refreshInfiniteCache(); gridRef.current.api.refreshInfiniteCache();
return true; return true;
}
dataRef.current = data;
return false;
}, },
[gridRef, scrolledToTop] [gridRef, scrolledToTop]
); );

View File

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

View File

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

View File

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

View File

@ -50,6 +50,7 @@ export const useOrderListData = ({
if (!gridRef.current?.api) { if (!gridRef.current?.api) {
return false; return false;
} }
if (dataRef.current?.length) {
if (!scrolledToTop.current) { if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt; const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) { if (createdAt) {
@ -61,6 +62,9 @@ export const useOrderListData = ({
dataRef.current = data; dataRef.current = data;
gridRef.current.api.refreshInfiniteCache(); gridRef.current.api.refreshInfiniteCache();
return true; return true;
}
dataRef.current = data;
return false;
}, },
[gridRef, scrolledToTop] [gridRef, scrolledToTop]
); );

View File

@ -24,11 +24,7 @@ import type {
ICellRendererParams, ICellRendererParams,
ValueFormatterParams, ValueFormatterParams,
} from 'ag-grid-community'; } from 'ag-grid-community';
import type { import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
AgGridReact,
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
import { forwardRef, useState } from 'react'; import { forwardRef, useState } from 'react';
import type { Orders_party_ordersConnection_edges_node } from '../'; import type { Orders_party_ordersConnection_edges_node } from '../';
@ -40,7 +36,7 @@ import { OrderEditDialog } from './order-edit-dialog';
import type { OrderWithMarket } from '../'; import type { OrderWithMarket } from '../';
import { OrderFeedback } from '../order-feedback'; import { OrderFeedback } from '../order-feedback';
type OrderListProps = AgGridReactProps | AgReactUiProps; type OrderListProps = AgGridReactProps;
export const OrderList = forwardRef<AgGridReact, OrderListProps>( export const OrderList = forwardRef<AgGridReact, OrderListProps>(
(props, ref) => { (props, ref) => {
@ -104,7 +100,7 @@ type OrderListTableValueFormatterParams = Omit<
data: OrderWithMarket | null; data: OrderWithMarket | null;
}; };
type OrderListTableProps = (AgGridReactProps | AgReactUiProps) & { type OrderListTableProps = AgGridReactProps & {
cancel: (order: OrderWithMarket) => void; cancel: (order: OrderWithMarket) => void;
setEditOrder: (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].error).toBe(error);
expect(callback.mock.calls[0][0].loading).toBe(false); expect(callback.mock.calls[0][0].loading).toBe(false);
subscription.reload(); subscription.reload();
expect(callback.mock.calls.length).toBe(3); expect(callback.mock.calls.length).toBe(2);
expect(callback.mock.calls[2][0].loading).toBe(true); expect(callback.mock.calls[1][0].loading).toBe(true);
await resolveQuery({ data: part1 }); await resolveQuery({ data: part1 });
expect(callback.mock.calls.length).toBe(3); expect(callback.mock.calls.length).toBe(2);
await resolveQuery({ data: part2 }); await resolveQuery({ data: part2 });
expect(callback.mock.calls.length).toBe(4); expect(callback.mock.calls.length).toBe(3);
expect(callback.mock.calls[3][0].data).toStrictEqual(data); expect(callback.mock.calls[2][0].data).toStrictEqual(data);
expect(callback.mock.calls[3][0].loading).toBe(false); expect(callback.mock.calls[2][0].loading).toBe(false);
expect(callback.mock.calls[3][0].error).toBeUndefined(); expect(callback.mock.calls[2][0].error).toBeUndefined();
subscription.unsubscribe(); subscription.unsubscribe();
}); });
}); });

View File

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

View File

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

View File

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