180 lines
5.0 KiB
TypeScript
180 lines
5.0 KiB
TypeScript
import { useEffect } from 'react'
|
|
import ReconnectingWebSocket, { CloseEvent } from 'reconnecting-websocket'
|
|
import { proxy, ref, useSnapshot } from 'valtio'
|
|
import { subscribeKey } from 'valtio/utils'
|
|
import { Select } from '.'
|
|
import state, { ILog, transactionsState } from '../state'
|
|
import { extractJSON } from '../utils/json'
|
|
import LogBox from './LogBox'
|
|
|
|
interface ISelect<T = string> {
|
|
label: string
|
|
value: T
|
|
}
|
|
|
|
export interface IStreamState {
|
|
selectedAccount: ISelect | null
|
|
status: 'idle' | 'opened' | 'closed'
|
|
statusChangeTimestamp?: number
|
|
logs: ILog[]
|
|
socket?: ReconnectingWebSocket
|
|
}
|
|
|
|
export const streamState = proxy<IStreamState>({
|
|
selectedAccount: null as ISelect | null,
|
|
status: 'idle',
|
|
logs: [] as ILog[]
|
|
})
|
|
|
|
const onOpen = (account: ISelect | null) => {
|
|
if (!account) {
|
|
return
|
|
}
|
|
// streamState.logs = [];
|
|
streamState.status = 'opened'
|
|
streamState.statusChangeTimestamp = Date.now()
|
|
|
|
pushLog(`Debug stream opened for account ${account?.value}`, {
|
|
type: 'success'
|
|
})
|
|
}
|
|
const onError = () => {
|
|
pushLog('Something went wrong! Check your connection and try again.', {
|
|
type: 'error'
|
|
})
|
|
}
|
|
const onClose = (e: CloseEvent) => {
|
|
// 999 = closed websocket connection by switching account
|
|
if (e.code !== 4999) {
|
|
pushLog(`Connection was closed. [code: ${e.code}]`, {
|
|
type: 'error'
|
|
})
|
|
}
|
|
streamState.status = 'closed'
|
|
streamState.statusChangeTimestamp = Date.now()
|
|
}
|
|
const onMessage = (event: any) => {
|
|
// Ping returns just account address, if we get that
|
|
// response we don't need to log anything
|
|
if (event.data !== streamState.selectedAccount?.value) {
|
|
pushLog(event.data)
|
|
}
|
|
}
|
|
|
|
let interval: NodeJS.Timer | null = null
|
|
|
|
const addListeners = (account: ISelect | null) => {
|
|
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
|
|
return
|
|
}
|
|
streamState.logs = []
|
|
if (account?.value) {
|
|
if (interval) {
|
|
clearInterval(interval)
|
|
}
|
|
if (streamState.socket) {
|
|
streamState.socket?.removeEventListener('open', () => onOpen(account))
|
|
streamState.socket?.removeEventListener('close', onClose)
|
|
streamState.socket?.removeEventListener('error', onError)
|
|
streamState.socket?.removeEventListener('message', onMessage)
|
|
}
|
|
|
|
streamState.socket = ref(
|
|
new ReconnectingWebSocket(
|
|
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
|
|
)
|
|
)
|
|
if (streamState.socket) {
|
|
interval = setInterval(() => {
|
|
streamState.socket?.send('')
|
|
}, 45000)
|
|
}
|
|
|
|
streamState.socket.addEventListener('open', () => onOpen(account))
|
|
streamState.socket.addEventListener('close', onClose)
|
|
streamState.socket.addEventListener('error', onError)
|
|
streamState.socket.addEventListener('message', onMessage)
|
|
}
|
|
}
|
|
|
|
subscribeKey(streamState, 'selectedAccount', addListeners)
|
|
|
|
const DebugStream = () => {
|
|
const { selectedAccount, logs } = useSnapshot(streamState)
|
|
const { activeHeader: activeTxTab } = useSnapshot(transactionsState)
|
|
const { accounts } = useSnapshot(state)
|
|
|
|
const accountOptions = accounts.map(acc => ({
|
|
label: acc.name,
|
|
value: acc.address
|
|
}))
|
|
|
|
const renderNav = () => (
|
|
<>
|
|
<Select
|
|
instanceId="DSAccount"
|
|
placeholder="Select account"
|
|
options={accountOptions}
|
|
hideSelectedOptions
|
|
value={selectedAccount}
|
|
onChange={acc => {
|
|
streamState.socket?.close(4999, 'Old connection closed because user switched account')
|
|
streamState.selectedAccount = acc as any
|
|
}}
|
|
css={{ width: '100%' }}
|
|
/>
|
|
</>
|
|
)
|
|
|
|
useEffect(() => {
|
|
const account = transactionsState.transactions.find(tx => tx.header === activeTxTab)?.state
|
|
.selectedAccount
|
|
|
|
if (account && account.value !== streamState.selectedAccount?.value)
|
|
streamState.selectedAccount = account
|
|
}, [activeTxTab])
|
|
|
|
const clearLog = () => {
|
|
streamState.logs = []
|
|
streamState.statusChangeTimestamp = Date.now()
|
|
}
|
|
|
|
return (
|
|
<LogBox enhanced renderNav={renderNav} title="Debug stream" logs={logs} clearLog={clearLog} />
|
|
)
|
|
}
|
|
|
|
export default DebugStream
|
|
|
|
export const pushLog = (str: any, opts: Partial<Pick<ILog, 'type'>> = {}): ILog | undefined => {
|
|
if (!str) return
|
|
if (typeof str !== 'string') throw Error('Unrecognized debug log stream!')
|
|
|
|
const match = str.match(/([\s\S]+(?:UTC|ISO|GMT[+|-]\d+))?\ ?([\s\S]*)/m)
|
|
const [_, tm, msg] = match || []
|
|
|
|
const timestamp = Date.parse(tm || '') || undefined
|
|
const timestring = !timestamp ? tm : new Date(timestamp).toLocaleTimeString()
|
|
|
|
const extracted = extractJSON(msg)
|
|
const message = !extracted ? msg : msg.slice(0, extracted.start) + msg.slice(extracted.end + 1)
|
|
|
|
const jsonData = extracted ? JSON.stringify(extracted.result, null, 2) : undefined
|
|
|
|
if (extracted?.result?.id?._Request?.includes('hooks-builder-req')) {
|
|
return
|
|
}
|
|
|
|
const { type = 'log' } = opts
|
|
const log: ILog = {
|
|
type,
|
|
message,
|
|
timestring,
|
|
jsonData,
|
|
defaultCollapsed: true
|
|
}
|
|
|
|
if (log) streamState.logs.push(log)
|
|
return log
|
|
}
|