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,65 +3,88 @@ import type { SocketData, SocketSubscriber } from 'lib/socket/types';
import appConfig from 'configs/app/config';
import { SECOND } from 'lib/consts';
interface InitParams {
onOpen?: (event: Event) => void;
onError?: (event: Event) => void;
onClose?: (event: Event) => void;
}
const OPEN_STATE = 1;
class Socket {
private socket: WebSocket | undefined;
private heartBeatIntervalId: number | undefined;
private lastHeartBeatTs: number | undefined;
private onReadyEvents: Array<SocketData> = [];
private channels: Record<string, Array<SocketSubscriber>> = {};
init({ onOpen, onError, onClose }: InitParams | undefined = {}) {
if (this.socket) {
return this;
}
private HEART_BEAT_INTERVAL = 30 * SECOND;
this.socket = new WebSocket(`${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2/websocket?vsn=2.0.0`);
private handleOpen = () => {
this.startHeartBeat();
this.socket.addEventListener('open', (event: Event) => {
this.startHeartBeat();
onOpen?.(event);
this.onReadyEvents.forEach((data) => this.socket?.send(JSON.stringify(data)));
this.onReadyEvents = [];
};
private handleMessage = (event: MessageEvent) => {
const data: SocketData = JSON.parse(event.data);
const channelId = data[2];
const eventId = data[3];
const payload = data[4];
const subscribers = this.channels[channelId];
subscribers
?.filter((subscriber) => subscriber.eventId ? subscriber.eventId === eventId : true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
?.forEach((subscriber) => subscriber.onMessage(payload as any));
if (channelId === 'phoenix' && eventId === 'phx_reply') {
const isOk = (payload as { status?: string } | undefined)?.status === 'ok';
isOk && (this.lastHeartBeatTs = Date.now());
}
};
this.onReadyEvents.forEach((data) => this.socket?.send(JSON.stringify(data)));
this.onReadyEvents = [];
});
private handleClose = () => {
this.beforeClose();
this.afterClose();
};
this.socket.addEventListener('message', (event) => {
const data: SocketData = JSON.parse(event.data);
private handleError = () => {
Object.values(this.channels).forEach((channel) => channel.forEach((subscriber) => subscriber.onError?.()));
};
const channelId = data[2];
const eventId = data[3];
const payload = data[4];
const subscribers = this.channels[channelId];
subscribers
?.filter((subscriber) => subscriber.eventId ? subscriber.eventId === eventId : true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
?.forEach((subscriber) => subscriber.onMessage(payload as any));
});
init() {
if (this.socket) {
return this;
}
this.socket.addEventListener('error', (event) => {
onError?.(event);
});
this.socket = new WebSocket(`${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2/websocket?vsn=2.0.0`);
this.socket.addEventListener('close', (event) => {
onClose?.(event);
});
this.socket.addEventListener('open', this.handleOpen);
this.socket.addEventListener('message', this.handleMessage);
this.socket.addEventListener('error', this.handleError);
this.socket.addEventListener('close', this.handleClose);
return this;
}
close() {
window.clearInterval(this.heartBeatIntervalId);
this.beforeClose();
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.onReadyEvents = [];
this.channels = {};
this.lastHeartBeatTs = undefined;
}
joinRoom(subscriber: SocketSubscriber) {
......@@ -96,9 +119,19 @@ class Socket {
private startHeartBeat() {
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', {} ];
this.socket?.send(JSON.stringify(data));
}, 30 * SECOND);
}, this.HEART_BEAT_INTERVAL);
}
private getChannelId(pattern: string, hash?: string) {
......
......@@ -3,13 +3,15 @@ import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketData = [ null, null, string, string, unknown ];
export type SocketSubscriber = SocketSubscribers.BlocksNewBlock |
SocketSubscribers.BlocksIndexStatus |
SocketSubscribers.BlockNewBlock |
SocketSubscribers.BlockNewBlock;
interface SocketSubscriberGeneric<Channel extends string, Event extends string, Payload> {
channelId: Channel;
eventId: Event;
onMessage: (payload: Payload) => void;
onClose?: () => void;
onError?: () => void;
hash?: string;
}
......
......@@ -36,7 +36,7 @@ export interface BlocksResponse {
next_page_params: {
block_number: number;
items_count: number;
};
} | null;
}
export interface BlockTransactionsResponse {
......
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 type { SocketSubscribers } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/accountQueries';
import useFetch from 'lib/hooks/useFetch';
import useSocketRoom from 'lib/hooks/useSocketRoom';
import BlocksList from 'ui/blocks/BlocksList';
import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile';
import BlocksTable from 'ui/blocks/BlocksTable';
......@@ -19,12 +21,43 @@ interface Props {
const BlocksContent = ({ type }: Props) => {
const fetch = useFetch();
const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError } = useQuery<unknown, unknown, BlocksResponse>(
[ QueryKeys.blocks, 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) {
return (
<>
......@@ -50,6 +83,7 @@ const BlocksContent = ({ type }: Props) => {
return (
<>
<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 above="lg" key="content-desktop"><BlocksTable data={ data.items }/></Show>
<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