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 { SECOND } from 'lib/consts';
......@@ -15,9 +15,9 @@ class Socket {
private socket: WebSocket | undefined;
private heartBeatIntervalId: number | undefined;
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) {
return this;
}
......@@ -36,12 +36,13 @@ class Socket {
const data: SocketData = JSON.parse(event.data);
const channelId = data[2];
const filterId = data[3];
const eventId = data[3];
const payload = data[4];
const subscribers = this.channels[channelId];
subscribers
?.filter((subscriber) => subscriber.filters ? subscriber.filters.includes(filterId) : true)
?.forEach((subscriber) => subscriber.onMessage(payload));
?.filter((subscriber) => subscriber.eventId ? subscriber.eventId === eventId : true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
?.forEach((subscriber) => subscriber.onMessage(payload as any));
});
this.socket.addEventListener('error', (event) => {
......@@ -63,8 +64,9 @@ class Socket {
this.channels = {};
}
joinRoom(id: string, subscriber: SocketChannelSubscriber) {
const data: SocketData = [ null, null, id, 'phx_join', {} ];
joinRoom(subscriber: SocketSubscriber) {
const channelId = this.getChannelId(subscriber.channelId, subscriber.hash);
const data: SocketData = [ null, null, channelId, 'phx_join', {} ];
if (this.socket?.readyState === OPEN_STATE) {
this.socket?.send(JSON.stringify(data));
......@@ -72,16 +74,16 @@ class Socket {
this.onReadyEvents.push(data);
}
if (this.channels[id]) {
this.channels[id].push(subscriber);
if (this.channels[channelId]) {
this.channels[channelId].push(subscriber);
} else {
this.channels[id] = [ subscriber ];
this.channels[channelId] = [ subscriber ];
}
}
leaveRoom(id: string) {
// todo_tom remove only specified subscriber
const data: SocketData = [ null, null, id, 'phx_leave', {} ];
leaveRoom(subscriber: SocketSubscriber) {
const channelId = this.getChannelId(subscriber.channelId, subscriber.hash);
const data: SocketData = [ null, null, channelId, 'phx_leave', {} ];
if (this.socket?.readyState === OPEN_STATE) {
this.socket?.send(JSON.stringify(data));
......@@ -89,7 +91,7 @@ class Socket {
this.onReadyEvents.push(data);
}
this.channels[id] = [];
this.channels[channelId]?.filter(({ onMessage }) => onMessage !== subscriber.onMessage);
}
private startHeartBeat() {
......@@ -98,6 +100,14 @@ class Socket {
this.socket?.send(JSON.stringify(data));
}, 30 * SECOND);
}
private getChannelId(pattern: string, hash?: string) {
if (!hash) {
return pattern;
}
return pattern.replace('[hash]', hash);
}
}
export default Socket;
export type SocketData = [ null, null, string, string, Record<string, unknown> ];
import type { NewBlockSocketResponse } from 'types/api/block';
export interface SocketChannelSubscriber {
filters?: Array<string>;
onMessage: (payload: unknown) => void;
export type SocketData = [ null, null, string, string, unknown ];
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 {
items_count: number;
} | null;
}
export interface NewBlockSocketResponse {
average_block_time: string;
block: Block;
}
......@@ -3,6 +3,8 @@ import * as Sentry from '@sentry/react';
import type { ChangeEvent } from 'react';
import React from 'react';
import type { SocketSubscribers } from 'lib/socket/types';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import useToast from 'lib/hooks/useToast';
......@@ -17,14 +19,22 @@ const Home = () => {
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const socket = (new Socket).init({});
socket.joinRoom('blocks:0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252', {
filters: [ 'new_block' ],
onMessage: () => {},
const socket = (new Socket).init();
const onMessage: SocketSubscribers.BlocksNewBlock['onMessage'] = () => {};
socket.joinRoom({
channelId: 'blocks:new_block',
eventId: 'new_block',
onMessage,
hash: '0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252',
});
return () => {
socket.leaveRoom('blocks:0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252');
socket.leaveRoom({
channelId: 'blocks:[hash]',
eventId: 'new_block',
hash: '0xdc4765d9dabf6c6c4908fe97e649ef1f05cb6252',
onMessage,
});
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