Commit 866cec6e authored by tom's avatar tom

more types

parent f642af8c
import React from 'react';
import { SECOND } from 'lib/consts';
const OPEN_STATE = 1;
interface Params {
onOpen?: (event: Event) => void;
onError?: (event: Event) => void;
onClose?: (event: Event) => void;
}
type SocketData = [ null, null, string, string, Record<string, unknown> ];
interface SocketChannelSubscriber {
filters?: Array<string>;
onMessage: (payload: unknown) => void;
}
export default function useApiSocket({ onOpen, onError, onClose }: Params) {
const socket = React.useRef<WebSocket>();
const onReadyEvents = React.useRef<Array<SocketData>>([]);
const channels = React.useRef<Record<string, Array<SocketChannelSubscriber>>>({});
function startHeartBeat() {
return window.setInterval(() => {
const data: SocketData = [ null, null, 'phoenix', 'heartbeat', {} ];
socket.current?.send(JSON.stringify(data));
}, 30 * SECOND);
}
const joinRoom = React.useCallback((id: string, subscriber: SocketChannelSubscriber) => {
const data: SocketData = [ null, null, id, 'phx_join', {} ];
if (socket.current?.readyState === OPEN_STATE) {
socket.current?.send(JSON.stringify(data));
} else {
onReadyEvents.current.push(data);
}
if (channels.current[id]) {
channels.current[id].push(subscriber);
} else {
channels.current[id] = [ subscriber ];
}
}, []);
const leaveRoom = React.useCallback((id: string) => {
const data: SocketData = [ null, null, id, 'phx_leave', {} ];
if (socket.current?.readyState === OPEN_STATE) {
socket.current?.send(JSON.stringify(data));
} else {
onReadyEvents.current.push(data);
}
channels.current[id] = [];
}, []);
React.useEffect(() => {
if (socket.current) {
socket.current.close();
}
// todo_tom pass host and base path from config
socket.current = new WebSocket('wss://blockscout.com/poa/core/socket/v2/websocket?vsn=2.0.0');
let heartBeatTimeoutId: number | undefined;
socket.current.addEventListener('open', (event: Event) => {
onOpen?.(event);
heartBeatTimeoutId = startHeartBeat();
onReadyEvents.current.forEach((data) => socket.current?.send(JSON.stringify(data)));
onReadyEvents.current = [];
});
socket.current.addEventListener('message', (event) => {
const data: SocketData = JSON.parse(event.data);
const channelId = data[2];
const filterId = data[3];
const payload = data[4];
const subscribers = channels.current[channelId];
subscribers
?.filter((subscriber) => subscriber.filters ? subscriber.filters.includes(filterId) : true)
?.forEach((subscriber) => subscriber.onMessage(payload));
});
socket.current.addEventListener('error', (event) => {
onError?.(event);
});
socket.current.addEventListener('close', (event) => {
onClose?.(event);
});
return () => {
window.clearInterval(heartBeatTimeoutId);
socket.current?.close();
};
}, [ onClose, onError, onOpen ]);
return { joinRoom, leaveRoom };
}
import type { SocketData, SocketChannelSubscriber } from 'lib/socket/types'; 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';
...@@ -15,9 +15,9 @@ class Socket { ...@@ -15,9 +15,9 @@ class Socket {
private socket: WebSocket | undefined; private socket: WebSocket | undefined;
private heartBeatIntervalId: number | undefined; private heartBeatIntervalId: number | undefined;
private onReadyEvents: Array<SocketData> = []; private onReadyEvents: Array<SocketData> = [];
private channels: Record<string, Array<SocketChannelSubscriber>> = {}; private channels: Record<string, Array<SocketSubscriber>> = {};
init({ onOpen, onError, onClose }: InitParams) { init({ onOpen, onError, onClose }: InitParams | undefined = {}) {
if (this.socket) { if (this.socket) {
return this; return this;
} }
...@@ -36,12 +36,13 @@ class Socket { ...@@ -36,12 +36,13 @@ class Socket {
const data: SocketData = JSON.parse(event.data); const data: SocketData = JSON.parse(event.data);
const channelId = data[2]; const channelId = data[2];
const filterId = data[3]; const eventId = data[3];
const payload = data[4]; const payload = data[4];
const subscribers = this.channels[channelId]; const subscribers = this.channels[channelId];
subscribers subscribers
?.filter((subscriber) => subscriber.filters ? subscriber.filters.includes(filterId) : true) ?.filter((subscriber) => subscriber.eventId ? subscriber.eventId === eventId : true)
?.forEach((subscriber) => subscriber.onMessage(payload)); // eslint-disable-next-line @typescript-eslint/no-explicit-any
?.forEach((subscriber) => subscriber.onMessage(payload as any));
}); });
this.socket.addEventListener('error', (event) => { this.socket.addEventListener('error', (event) => {
...@@ -63,8 +64,9 @@ class Socket { ...@@ -63,8 +64,9 @@ class Socket {
this.channels = {}; this.channels = {};
} }
joinRoom(id: string, subscriber: SocketChannelSubscriber) { joinRoom(subscriber: SocketSubscriber) {
const data: SocketData = [ null, null, id, 'phx_join', {} ]; const channelId = this.getChannelId(subscriber.channelId, subscriber.hash);
const data: SocketData = [ null, null, channelId, 'phx_join', {} ];
if (this.socket?.readyState === OPEN_STATE) { if (this.socket?.readyState === OPEN_STATE) {
this.socket?.send(JSON.stringify(data)); this.socket?.send(JSON.stringify(data));
...@@ -72,16 +74,16 @@ class Socket { ...@@ -72,16 +74,16 @@ class Socket {
this.onReadyEvents.push(data); this.onReadyEvents.push(data);
} }
if (this.channels[id]) { if (this.channels[channelId]) {
this.channels[id].push(subscriber); this.channels[channelId].push(subscriber);
} else { } else {
this.channels[id] = [ subscriber ]; this.channels[channelId] = [ subscriber ];
} }
} }
leaveRoom(id: string) { leaveRoom(subscriber: SocketSubscriber) {
// todo_tom remove only specified subscriber const channelId = this.getChannelId(subscriber.channelId, subscriber.hash);
const data: SocketData = [ null, null, id, 'phx_leave', {} ]; const data: SocketData = [ null, null, channelId, 'phx_leave', {} ];
if (this.socket?.readyState === OPEN_STATE) { if (this.socket?.readyState === OPEN_STATE) {
this.socket?.send(JSON.stringify(data)); this.socket?.send(JSON.stringify(data));
...@@ -89,7 +91,7 @@ class Socket { ...@@ -89,7 +91,7 @@ class Socket {
this.onReadyEvents.push(data); this.onReadyEvents.push(data);
} }
this.channels[id] = []; this.channels[channelId]?.filter(({ onMessage }) => onMessage !== subscriber.onMessage);
} }
private startHeartBeat() { private startHeartBeat() {
...@@ -98,6 +100,14 @@ class Socket { ...@@ -98,6 +100,14 @@ class Socket {
this.socket?.send(JSON.stringify(data)); this.socket?.send(JSON.stringify(data));
}, 30 * SECOND); }, 30 * SECOND);
} }
private getChannelId(pattern: string, hash?: string) {
if (!hash) {
return pattern;
}
return pattern.replace('[hash]', hash);
}
} }
export default Socket; export default Socket;
export type SocketData = [ null, null, string, string, Record<string, unknown> ]; import type { NewBlockSocketResponse } from 'types/api/block';
export interface SocketChannelSubscriber { export type SocketData = [ null, null, string, string, unknown ];
filters?: Array<string>;
onMessage: (payload: unknown) => void; export type SocketSubscriber = SocketSubscribers.BlocksNewBlock |
SocketSubscribers.BlocksIndexStatus |
SocketSubscribers.BlockNewBlock;
interface SocketSubscriberGeneric<Channel extends string, Event extends string, Payload> {
channelId: Channel;
eventId: Event;
onMessage: (payload: Payload) => void;
hash?: string;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SocketSubscribers {
export type BlocksNewBlock = SocketSubscriberGeneric<'blocks:new_block', 'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketSubscriberGeneric<'blocks:indexing', 'index_status', {finished: boolean; ratio: string}>;
export type BlockNewBlock = SocketSubscriberGeneric<'blocks:[hash]', 'new_block', NewBlockSocketResponse>;
} }
...@@ -47,3 +47,8 @@ export interface BlockTransactionsResponse { ...@@ -47,3 +47,8 @@ export interface BlockTransactionsResponse {
items_count: number; items_count: number;
} | null; } | null;
} }
export interface NewBlockSocketResponse {
average_block_time: string;
block: Block;
}
...@@ -3,6 +3,8 @@ import * as Sentry from '@sentry/react'; ...@@ -3,6 +3,8 @@ import * as Sentry from '@sentry/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { SocketSubscribers } from 'lib/socket/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
...@@ -17,14 +19,22 @@ const Home = () => { ...@@ -17,14 +19,22 @@ const Home = () => {
const [ token, setToken ] = React.useState(''); const [ token, setToken ] = React.useState('');
React.useEffect(() => { React.useEffect(() => {
const socket = (new Socket).init({}); const socket = (new Socket).init();
socket.joinRoom('blocks:0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252', { const onMessage: SocketSubscribers.BlocksNewBlock['onMessage'] = () => {};
filters: [ 'new_block' ], socket.joinRoom({
onMessage: () => {}, channelId: 'blocks:new_block',
eventId: 'new_block',
onMessage,
hash: '0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252',
}); });
return () => { return () => {
socket.leaveRoom('blocks:0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252'); socket.leaveRoom({
channelId: 'blocks:[hash]',
eventId: 'new_block',
hash: '0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252',
onMessage,
});
socket.close(); socket.close();
}; };
}, []); }, []);
......
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