Commit 684959dd authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #846 from blockscout/socket-changes

change socket model
parents 8e11098f 83723cb1
...@@ -49,9 +49,9 @@ export namespace SocketMessage { ...@@ -49,9 +49,9 @@ export namespace SocketMessage {
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>; export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>; export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transactions: Array<Transaction> }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transactions: Array<Transaction> }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array<TokenTransfer> }>;
export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>; export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>; export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>; export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>;
......
...@@ -251,3 +251,26 @@ export const l2tx: Transaction = { ...@@ -251,3 +251,26 @@ export const l2tx: Transaction = {
l1_gas_used: '17060', l1_gas_used: '17060',
l1_fee: '1584574188135760', l1_fee: '1584574188135760',
}; };
export const base2 = {
...base,
hash: '0x02d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
},
};
export const base3 = {
...base,
hash: '0x12d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
},
};
export const base4 = {
...base,
hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
};
...@@ -5,6 +5,8 @@ import { WebSocketServer } from 'ws'; ...@@ -5,6 +5,8 @@ import { WebSocketServer } from 'ws';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract'; import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
type ReturnType = () => Promise<WebSocket>; type ReturnType = () => Promise<WebSocket>;
...@@ -58,11 +60,14 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => { ...@@ -58,11 +60,14 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_transfer', payload: { token_transfers: Array<TokenTransfer> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void { export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([ socket.send(JSON.stringify([
...channel, ...channel,
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { erc1155A } from 'mocks/tokens/tokenTransfer'; import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers'; import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) + const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859'; '?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: CURRENT_ADDRESS, token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
}, },
}; };
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('with token filter and pagination', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -37,7 +48,7 @@ test('with token filter and pagination', async({ mount, page }) => { ...@@ -37,7 +48,7 @@ test('with token filter and pagination', async({ mount, page }) => {
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
...@@ -57,7 +68,7 @@ test.describe('mobile', () => { ...@@ -57,7 +68,7 @@ test.describe('mobile', () => {
test('with token filter and pagination', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -74,7 +85,7 @@ test.describe('mobile', () => { ...@@ -74,7 +85,7 @@ test.describe('mobile', () => {
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
...@@ -88,3 +99,160 @@ test.describe('mobile', () => { ...@@ -88,3 +99,160 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig: hooksConfigNoToken },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with overload', async({ mount, page, createSocket }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigNoToken },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
test('without overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(
socket,
channel,
'token_transfer',
{ token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20, tokenTransferMock.erc1155C, tokenTransferMock.erc721 ] },
);
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
});
...@@ -63,7 +63,13 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: ...@@ -63,7 +63,13 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
return true; return true;
}; };
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
// for tests only
overloadCount?: number;
}
const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -117,11 +123,26 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -117,11 +123,26 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
if (data?.items && data.items.length >= OVERLOAD_COUNT) { const newItems: Array<TokenTransfer> = [];
if (matchFilters(filters, payload.token_transfer, currentAddress)) { let newCount = 0;
setNewItemsCount(prev => prev + 1);
payload.token_transfers.forEach(transfer => {
if (data?.items && data.items.length + newItems.length >= overloadCount) {
if (matchFilters(filters, transfer, currentAddress)) {
newCount++;
}
} else {
if (matchFilters(filters, transfer, currentAddress)) {
newItems.push(transfer);
}
} }
} else { });
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
}
if (newItems.length > 0) {
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }), getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }),
(prevData: AddressTokenTransferResponse | undefined) => { (prevData: AddressTokenTransferResponse | undefined) => {
...@@ -129,19 +150,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -129,19 +150,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return; return;
} }
if (!matchFilters(filters, payload.token_transfer, currentAddress)) {
return prevData;
}
return { return {
...prevData, ...prevData,
items: [ items: [
payload.token_transfer, ...newItems,
...prevData.items, ...prevData.items,
], ],
}; };
}); },
);
} }
}; };
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { base as txMock } from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs'; import AddressTxs from './AddressTxs';
const API_URL = buildApiUrl('address_txs', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }); const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: CURRENT_ADDRESS },
}, },
}; };
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('address txs +@mobile +@desktop-xl', async({ mount, page }) => { test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ txMock, txMock ], next_page_params: { block: 1 } }), body: JSON.stringify({ items: [ txMock.base, txMock.base ], next_page_params: { block: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -32,3 +43,167 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => { ...@@ -32,3 +43,167 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with update', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.pending ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base, txMock.base2 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('2 ')).toBe(true);
});
test('without overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
});
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { Transaction } from 'types/api/transaction';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
...@@ -26,7 +27,27 @@ const OVERLOAD_COUNT = 75; ...@@ -26,7 +27,27 @@ const OVERLOAD_COUNT = 75;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => {
if (!filterValue) {
return true;
}
if (filterValue === 'from') {
return transaction.from.hash === address;
}
if (filterValue === 'to') {
return transaction.to?.hash === address;
}
};
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
// for tests only
overloadCount?: number;
}
const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -62,16 +83,6 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -62,16 +83,6 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
if (addressTxsQuery.data?.items && addressTxsQuery.data.items.length >= OVERLOAD_COUNT) {
if (
!filterValue ||
(filterValue === 'from' && payload.transaction.from.hash === currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash === currentAddress)
) {
setNewItemsCount(prev => prev + 1);
}
}
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }), getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => { (prevData: AddressTransactionsResponse | undefined) => {
...@@ -79,30 +90,33 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -79,30 +90,33 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
return; return;
} }
const currIndex = prevData.items.findIndex((tx) => tx.hash === payload.transaction.hash); const newItems: Array<Transaction> = [];
let newCount = 0;
if (currIndex > -1) {
prevData.items[currIndex] = payload.transaction; payload.transactions.forEach(tx => {
return prevData; const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash);
}
if (currIndex > -1) {
if (prevData.items.length >= OVERLOAD_COUNT) { prevData.items[currIndex] = tx;
return prevData; } else {
} if (matchFilter(filterValue, tx, currentAddress)) {
if (newItems.length + prevData.items.length >= overloadCount) {
if (filterValue) { newCount++;
if ( } else {
(filterValue === 'from' && payload.transaction.from.hash !== currentAddress) || newItems.push(tx);
(filterValue === 'to' && payload.transaction.to?.hash !== currentAddress) }
) { }
return prevData;
} }
});
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
} }
return { return {
...prevData, ...prevData,
items: [ items: [
payload.transaction, ...newItems,
...prevData.items, ...prevData.items,
], ],
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment