Commit 49734a3e authored by tom's avatar tom

add socket to block list

parent 866cec6e
import React from 'react';
import type { SocketSubscriber } from 'lib/socket/types';
import Socket from 'lib/socket/Socket';
type Params = SocketSubscriber & {
isDisabled: boolean;
}
export default function useSocketRoom({ isDisabled, channelId, eventId, onMessage, onClose, onError }: Params) {
React.useEffect(() => {
if (isDisabled) {
return;
}
const room = {
channelId,
eventId,
onMessage,
onClose,
onError,
} as SocketSubscriber;
const socket = (new Socket).init();
socket.joinRoom(room);
return () => {
socket.leaveRoom(room);
socket.close();
};
}, [ channelId, eventId, isDisabled, onClose, onError, onMessage ]);
}
...@@ -3,36 +3,25 @@ import type { SocketData, SocketSubscriber } from 'lib/socket/types'; ...@@ -3,36 +3,25 @@ import type { SocketData, SocketSubscriber } from 'lib/socket/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
interface InitParams {
onOpen?: (event: Event) => void;
onError?: (event: Event) => void;
onClose?: (event: Event) => void;
}
const OPEN_STATE = 1; const OPEN_STATE = 1;
class Socket { class Socket {
private socket: WebSocket | undefined; private socket: WebSocket | undefined;
private heartBeatIntervalId: number | undefined; private heartBeatIntervalId: number | undefined;
private lastHeartBeatTs: number | undefined;
private onReadyEvents: Array<SocketData> = []; private onReadyEvents: Array<SocketData> = [];
private channels: Record<string, Array<SocketSubscriber>> = {}; private channels: Record<string, Array<SocketSubscriber>> = {};
init({ onOpen, onError, onClose }: InitParams | undefined = {}) { private HEART_BEAT_INTERVAL = 30 * SECOND;
if (this.socket) {
return this;
}
this.socket = new WebSocket(`${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2/websocket?vsn=2.0.0`);
this.socket.addEventListener('open', (event: Event) => { private handleOpen = () => {
this.startHeartBeat(); this.startHeartBeat();
onOpen?.(event);
this.onReadyEvents.forEach((data) => this.socket?.send(JSON.stringify(data))); this.onReadyEvents.forEach((data) => this.socket?.send(JSON.stringify(data)));
this.onReadyEvents = []; this.onReadyEvents = [];
}); };
this.socket.addEventListener('message', (event) => { private handleMessage = (event: MessageEvent) => {
const data: SocketData = JSON.parse(event.data); const data: SocketData = JSON.parse(event.data);
const channelId = data[2]; const channelId = data[2];
...@@ -43,25 +32,59 @@ class Socket { ...@@ -43,25 +32,59 @@ class Socket {
?.filter((subscriber) => subscriber.eventId ? subscriber.eventId === eventId : true) ?.filter((subscriber) => subscriber.eventId ? subscriber.eventId === eventId : true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
?.forEach((subscriber) => subscriber.onMessage(payload as any)); ?.forEach((subscriber) => subscriber.onMessage(payload as any));
});
this.socket.addEventListener('error', (event) => { if (channelId === 'phoenix' && eventId === 'phx_reply') {
onError?.(event); const isOk = (payload as { status?: string } | undefined)?.status === 'ok';
}); isOk && (this.lastHeartBeatTs = Date.now());
}
};
private handleClose = () => {
this.beforeClose();
this.afterClose();
};
private handleError = () => {
Object.values(this.channels).forEach((channel) => channel.forEach((subscriber) => subscriber.onError?.()));
};
init() {
if (this.socket) {
return this;
}
this.socket = new WebSocket(`${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2/websocket?vsn=2.0.0`);
this.socket.addEventListener('close', (event) => { this.socket.addEventListener('open', this.handleOpen);
onClose?.(event); this.socket.addEventListener('message', this.handleMessage);
}); this.socket.addEventListener('error', this.handleError);
this.socket.addEventListener('close', this.handleClose);
return this; return this;
} }
close() { close() {
window.clearInterval(this.heartBeatIntervalId); this.beforeClose();
this.socket?.close(); this.socket?.close();
this.afterClose();
}
beforeClose() {
window.clearInterval(this.heartBeatIntervalId);
this.socket?.removeEventListener('open', this.handleOpen);
this.socket?.removeEventListener('message', this.handleMessage);
this.socket?.removeEventListener('error', this.handleError);
this.socket?.removeEventListener('close', this.handleClose);
Object.values(this.channels).forEach((channel) => channel.forEach((subscriber) => subscriber.onClose?.()));
}
afterClose() {
this.socket = undefined; this.socket = undefined;
this.onReadyEvents = []; this.onReadyEvents = [];
this.channels = {}; this.channels = {};
this.lastHeartBeatTs = undefined;
} }
joinRoom(subscriber: SocketSubscriber) { joinRoom(subscriber: SocketSubscriber) {
...@@ -96,9 +119,19 @@ class Socket { ...@@ -96,9 +119,19 @@ class Socket {
private startHeartBeat() { private startHeartBeat() {
this.heartBeatIntervalId = window.setInterval(() => { this.heartBeatIntervalId = window.setInterval(() => {
if (this.socket?.readyState !== OPEN_STATE) {
return;
}
if (this.lastHeartBeatTs && Date.now() - this.lastHeartBeatTs > this.HEART_BEAT_INTERVAL) {
// if we didn't receive response to the last heartbeat
this.close();
return;
}
const data: SocketData = [ null, null, 'phoenix', 'heartbeat', {} ]; const data: SocketData = [ null, null, 'phoenix', 'heartbeat', {} ];
this.socket?.send(JSON.stringify(data)); this.socket?.send(JSON.stringify(data));
}, 30 * SECOND); }, this.HEART_BEAT_INTERVAL);
} }
private getChannelId(pattern: string, hash?: string) { private getChannelId(pattern: string, hash?: string) {
......
...@@ -3,13 +3,15 @@ import type { NewBlockSocketResponse } from 'types/api/block'; ...@@ -3,13 +3,15 @@ import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketData = [ null, null, string, string, unknown ]; export type SocketData = [ null, null, string, string, unknown ];
export type SocketSubscriber = SocketSubscribers.BlocksNewBlock | export type SocketSubscriber = SocketSubscribers.BlocksNewBlock |
SocketSubscribers.BlocksIndexStatus | SocketSubscribers.BlockNewBlock |
SocketSubscribers.BlockNewBlock; SocketSubscribers.BlockNewBlock;
interface SocketSubscriberGeneric<Channel extends string, Event extends string, Payload> { interface SocketSubscriberGeneric<Channel extends string, Event extends string, Payload> {
channelId: Channel; channelId: Channel;
eventId: Event; eventId: Event;
onMessage: (payload: Payload) => void; onMessage: (payload: Payload) => void;
onClose?: () => void;
onError?: () => void;
hash?: string; hash?: string;
} }
......
...@@ -36,7 +36,7 @@ export interface BlocksResponse { ...@@ -36,7 +36,7 @@ export interface BlocksResponse {
next_page_params: { next_page_params: {
block_number: number; block_number: number;
items_count: number; items_count: number;
}; } | null;
} }
export interface BlockTransactionsResponse { export interface BlockTransactionsResponse {
......
import { Box, Text, Show, Alert, Skeleton } from '@chakra-ui/react'; import { Box, Text, Show, Alert, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { SocketSubscribers } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block'; import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries'; import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useSocketRoom from 'lib/hooks/useSocketRoom';
import BlocksList from 'ui/blocks/BlocksList'; import BlocksList from 'ui/blocks/BlocksList';
import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile'; import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile';
import BlocksTable from 'ui/blocks/BlocksTable'; import BlocksTable from 'ui/blocks/BlocksTable';
...@@ -19,12 +21,43 @@ interface Props { ...@@ -19,12 +21,43 @@ interface Props {
const BlocksContent = ({ type }: Props) => { const BlocksContent = ({ type }: Props) => {
const fetch = useFetch(); const fetch = useFetch();
const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>(
[ QueryKeys.blocks, type ], [ QueryKeys.blocks, type ],
async() => await fetch(`/node-api/blocks${ type ? `?type=${ type }` : '' }`), async() => await fetch(`/node-api/blocks${ type ? `?type=${ type }` : '' }`),
); );
const handleNewBlockMessage: SocketSubscribers.BlocksNewBlock['onMessage'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.blocks, type ], (prevData: BlocksResponse | undefined) => {
if (!prevData) {
return {
items: [ payload.block ],
next_page_params: null,
};
}
return { ...prevData, items: [ payload.block, ...prevData.items ] };
});
}, [ queryClient, type ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new blocks.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new blocks. Please click here to refresh the page.');
}, []);
useSocketRoom({
channelId: 'blocks:new_block',
eventId: 'new_block',
onMessage: handleNewBlockMessage,
onClose: handleSocketClose,
onError: handleSocketError,
isDisabled: isLoading || isError,
});
if (isLoading) { if (isLoading) {
return ( return (
<> <>
...@@ -50,6 +83,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -50,6 +83,7 @@ const BlocksContent = ({ type }: Props) => {
return ( return (
<> <>
<Text as="span">Total of { data.items[0].height.toLocaleString() } blocks</Text> <Text as="span">Total of { data.items[0].height.toLocaleString() } blocks</Text>
{ socketAlert && <Alert status="warning" mt={ 8 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show> <Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show>
<Show above="lg" key="content-desktop"><BlocksTable data={ data.items }/></Show> <Show above="lg" key="content-desktop"><BlocksTable data={ data.items }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}> <Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
......
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